Skip to content

Commit

Permalink
Merge 5d70107 into 1a366bc
Browse files Browse the repository at this point in the history
  • Loading branch information
jonmmease committed Aug 27, 2020
2 parents 1a366bc + 5d70107 commit 17f7727
Show file tree
Hide file tree
Showing 7 changed files with 670 additions and 0 deletions.
239 changes: 239 additions & 0 deletions holoviews/core/decollate.py
@@ -0,0 +1,239 @@
import param
from .operation import OperationCallable
from .. import (
Layout, DynamicMap, Element, Callable, Overlay, GridSpace, NdOverlay, HoloMap
)
from . import ViewableTree
from collections import namedtuple
from ..streams import Stream, Derived

from ..plotting.util import initialize_dynamic

Expr = namedtuple("HoloviewsExpr", ["fn", "args", "kwargs"])
StreamIndex = namedtuple("StreamIndex", ["index"])
KDimIndex = namedtuple("KDim", ["index"])


def to_expr_extract_streams(
hvobj, kdims, streams, original_streams, stream_mapping, container_key=None
):
"""
Build a HoloViewsExpr expression tree from a potentially nested dynamic
HoloViews object, extracting the streams and replacing them with StreamIndex
objects.
This function is recursive an assumes that initialize_dynamic has already
been called on the input object.
Args:
hvobj: Element or DynamicMap or Layout
Potentially dynamic HoloViews object to represent as a HoloviewsExpr
kdims: list of Dimensions
List that DynamicMap key-dimension objects should be added to
streams: list of Stream
List that cloned extracted streams should be added to
original_streams: list of Stream
List that original extracted streams should be added to
stream_mapping: dict
dict to be populated with mappings from container keys to extracted Stream
objects, as described by the Callable parameter of the same name.
container_key: int or tuple
key into parent container that is associated to hvobj, or None if hvobj is
not in a container
Returns:
HoloviewsExpr expression representing hvobj if hvobj is dynamic. Otherwise,
reutrn hvobj itself
"""
if isinstance(hvobj, DynamicMap):
args = []
kwargs = []
dm_streams = hvobj.streams

# Process callback inputs recursively
input_exprs = [
to_expr_extract_streams(
v,
kdims,
streams,
original_streams,
stream_mapping,
container_key=container_key,
)
for i, v in enumerate(hvobj.callback.inputs)
]
# Record all key dimensions
kdim_args = []
for kdim in hvobj.kdims:
if kdim in kdims:
# Find index to existing kdim
idx = kdims.index(kdim)
kdim_index = KDimIndex(index=idx)

# Overwrite so that we end up with dimension object highest in the
# object tree
kdims[idx] = kdim
else:
# Add new kdim index
kdim_index = KDimIndex(index=len(kdims))
kdims.append(kdim)
kdim_args.append(kdim_index)

# Determine function
expand_kwargs = True
if len(input_exprs) > 1:
fn = Overlay
args.extend([input_exprs])
elif isinstance(hvobj.callback, OperationCallable):
fn = hvobj.callback.operation.instance(streams=[])
fn.dynamic = False
args.extend(input_exprs)

if "kwargs" in fn.param:
expand_kwargs = False
if "kwargs" in hvobj.callback.operation_kwargs:
kwargs.append(hvobj.callback.operation_kwargs["kwargs"])
else:
# Preserve custom operation kwargs
if hvobj.callback.operation_kwargs:
kwargs.append(hvobj.callback.operation_kwargs)
else:
fn = hvobj.callback.callable
args.extend(kdim_args)

for dm_stream in dm_streams:
stream_arg = to_expr_extract_streams(
dm_stream, kdims, streams, original_streams,
stream_mapping, container_key,
)
if hvobj.positional_stream_args:
args.append(stream_arg)
else:
kwargs.append(stream_arg)

if expand_kwargs:
expr = Expr(fn, args, kwargs)
else:
expr = Expr(fn, args, [{"kwargs": Expr(dict, [], kwargs)}])
return expr

elif isinstance(hvobj, Stream):
if isinstance(hvobj, Derived):
stream_arg_fn = hvobj.transform_function
stream_indexes = []
for input_stream in hvobj.input_streams:
stream_indexes.append(
to_expr_extract_streams(
input_stream, kdims, streams, original_streams,
stream_mapping, container_key,
)
)
constants = hvobj.constants
return Expr(
stream_arg_fn, [stream_indexes, constants], []
)
else:
# Get index for stream
# Compute stream index
if hvobj in original_streams:
# Reuse index to existing stream
stream_index = StreamIndex(index=original_streams.index(hvobj))
else:
# Add new stream
stream_index = StreamIndex(index=len(streams))
cloned_stream = hvobj.clone()
original_streams.append(hvobj)
streams.append(cloned_stream)
if container_key is not None:
stream_mapping.setdefault(container_key, []).append(cloned_stream)
return stream_index

elif isinstance(hvobj, (Layout, GridSpace, NdOverlay, HoloMap, Overlay)):
fn = hvobj.clone(data={}).clone
args = []
data_expr = []
for i, (key, v) in enumerate(hvobj.data.items()):
el = to_expr_extract_streams(
v, kdims, streams, original_streams, stream_mapping, i
)

# Replace "DynamicMap" with type of the non-dynamic return element
if isinstance(v, DynamicMap):
initialize_dynamic(v)
if (v.type is not None and
isinstance(key, tuple) and
isinstance(key[0], str)):
type_str = v.type.__name__
key = (key[0].replace("DynamicMap", type_str), "I")

data_expr.append((key, el))

if isinstance(hvobj, ViewableTree):
# Use _process_items to ensure that keys are unique
data_expr = ViewableTree._process_items(data_expr)

kwargs = [{"data": data_expr}]
return Expr(fn, args, kwargs)
elif isinstance(hvobj, Element):
return hvobj.clone(link=False)
else:
raise NotImplementedError("Type {typ} not implemented".format(typ=type(hvobj)))


def expr_to_fn_of_stream_contents(expr, nkdims):
def eval_expr(expr, kdim_values, stream_values):
if isinstance(expr, Expr):
fn = expr.fn
args = [eval_expr(arg, kdim_values, stream_values) for arg in expr.args]
kwargs_list = [eval_expr(kwarg, kdim_values, stream_values) for kwarg in
expr.kwargs]
kwargs = dict()
for kwargs_el in kwargs_list:
kwargs.update(**eval_expr(kwargs_el, kdim_values, stream_values))
# For a ParameterizedFunction (e.g. an Operation), drop keys that are not
# accepted as params to avoid warnings
if isinstance(fn, param.ParameterizedFunction):
kwargs = {k: v for k, v in kwargs.items() if k in fn.param}
return fn(*args, **kwargs)
elif isinstance(expr, StreamIndex):
return stream_values[expr.index]
elif isinstance(expr, KDimIndex):
return kdim_values[expr.index]
elif isinstance(expr, dict):
return {k: eval_expr(v, kdim_values, stream_values) for k, v in expr.items()}
elif isinstance(expr, (list, tuple)):
return type(expr)([eval_expr(v, kdim_values, stream_values) for v in expr])
else:
return expr

def expr_fn(*args):
kdim_values = args[:nkdims]
stream_values = args[nkdims:]
return eval_expr(expr, kdim_values, stream_values)

return expr_fn


def decollate(hvobj):
"""
Decollate transforms a potentially nested dynamic HoloViews object into single
DynamicMap that returns a non-dynamic HoloViews object. All nested streams in the
input object are copied and attached to the resulting DynamicMap.
Args:
hvobj: Holoviews object
Returns:
DynamicMap
"""
kdims = []
original_streams = []
streams = []
stream_mapping = {}
initialize_dynamic(hvobj)
expr = to_expr_extract_streams(hvobj, kdims, streams, original_streams, stream_mapping)

expr_fn = expr_to_fn_of_stream_contents(expr, nkdims=len(kdims))
callback = Callable(expr_fn, stream_mapping=stream_mapping)
return DynamicMap(
callback, kdims=kdims, streams=streams, positional_stream_args=True
)
18 changes: 18 additions & 0 deletions holoviews/core/layout.py
Expand Up @@ -436,6 +436,24 @@ def __init__(self, items=None, identifier=None, parent=None, **kwargs):
self.__dict__['_max_cols'] = 4
super(Layout, self).__init__(items, identifier, parent, **kwargs)

def decollate(self):
"""Packs Layout of DynamicMaps into a single DynamicMap that returns a Layout
Decollation allows packing a Layout of DynamicMaps into a single DynamicMap
that returns a Layout of simple (non-dynamic) elements. All nested streams are
lifted to the resulting DynamicMap, and are available in the `streams`
property. The `callback` property of the resulting DynamicMap is a pure,
stateless function of the stream values. To avoid stream parameter name
conflicts, the resulting DynamicMap is configured with
positional_stream_args=True, and the callback function accepts stream values
as positional dict arguments.
Returns:
DynamicMap that returns a Layout
"""
from .decollate import decollate
return decollate(self)

@property
def shape(self):
"Tuple indicating the number of rows and columns in the Layout."
Expand Down
37 changes: 37 additions & 0 deletions holoviews/core/overlay.py
Expand Up @@ -200,6 +200,24 @@ def collate(self):
"""
return reduce(lambda x,y: x*y, self.values())

def decollate(self):
"""Packs Overlay of DynamicMaps into a single DynamicMap that returns an Overlay
Decollation allows packing an Overlay of DynamicMaps into a single DynamicMap
that returns an Overlay of simple (non-dynamic) elements. All nested streams
are lifted to the resulting DynamicMap, and are available in the `streams`
property. The `callback` property of the resulting DynamicMap is a pure,
stateless function of the stream values. To avoid stream parameter name
conflicts, the resulting DynamicMap is configured with
positional_stream_args=True, and the callback function accepts stream values
as positional dict arguments.
Returns:
DynamicMap that returns an Overlay
"""
from .decollate import decollate
return decollate(self)

@property
def group(self):
if self._group:
Expand Down Expand Up @@ -293,6 +311,25 @@ class NdOverlay(Overlayable, UniformNdMapping, CompositeOverlay):
def __init__(self, overlays=None, kdims=None, **params):
super(NdOverlay, self).__init__(overlays, kdims=kdims, **params)

def decollate(self):
"""Packs NdOverlay of DynamicMaps into a single DynamicMap that returns an
NdOverlay
Decollation allows packing a NdOverlay of DynamicMaps into a single DynamicMap
that returns an NdOverlay of simple (non-dynamic) elements. All nested streams
are lifted to the resulting DynamicMap, and are available in the `streams`
property. The `callback` property of the resulting DynamicMap is a pure,
stateless function of the stream values. To avoid stream parameter name
conflicts, the resulting DynamicMap is configured with
positional_stream_args=True, and the callback function accepts stream values
as positional dict arguments.
Returns:
DynamicMap that returns an NdOverlay
"""
from .decollate import decollate
return decollate(self)


__all__ = list(set([_k for _k, _v in locals().items()
if isinstance(_v, type) and issubclass(_v, Dimensioned)])) + ['Overlayable']
55 changes: 55 additions & 0 deletions holoviews/core/spaces.py
Expand Up @@ -350,6 +350,25 @@ def collate(self, merge_type=None, drop=[], drop_constant=False):
return Collator(self, merge_type=merge_type, drop=drop,
drop_constant=drop_constant)()

def decollate(self):
"""Packs HoloMap of DynamicMaps into a single DynamicMap that returns an
HoloMap
Decollation allows packing a HoloMap of DynamicMaps into a single DynamicMap
that returns an HoloMap of simple (non-dynamic) elements. All nested streams
are lifted to the resulting DynamicMap, and are available in the `streams`
property. The `callback` property of the resulting DynamicMap is a pure,
stateless function of the stream values. To avoid stream parameter name
conflicts, the resulting DynamicMap is configured with
positional_stream_args=True, and the callback function accepts stream values
as positional dict arguments.
Returns:
DynamicMap that returns an HoloMap
"""
from .decollate import decollate
return decollate(self)


def sample(self, samples=[], bounds=None, **sample_values):
"""Samples element values at supplied coordinates.
Expand Down Expand Up @@ -1488,6 +1507,24 @@ def split_overlay_callback(obj, overlay_key=key, overlay_el=el, **kwargs):
dmaps.append(dmap)
return keys, dmaps

def decollate(self):
"""Packs DynamicMap of nested DynamicMaps into a single DynamicMap that
returns a non-dynamic element
Decollation allows packing a DynamicMap of nested DynamicMaps into a single
DynamicMap that returns a simple (non-dynamic) element. All nested streams are
lifted to the resulting DynamicMap, and are available in the `streams`
property. The `callback` property of the resulting DynamicMap is a pure,
stateless function of the stream values. To avoid stream parameter name
conflicts, the resulting DynamicMap is configured with
positional_stream_args=True, and the callback function accepts stream values
as positional dict arguments.
Returns:
DynamicMap that returns a non-dynamic element
"""
from .decollate import decollate
return decollate(self)

def collate(self):
"""Unpacks DynamicMap into container of DynamicMaps
Expand Down Expand Up @@ -1936,6 +1973,24 @@ def shape(self):
return (len(keys), 1)
return len(set(k[0] for k in keys)), len(set(k[1] for k in keys))

def decollate(self):
"""Packs GridSpace of DynamicMaps into a single DynamicMap that returns a
GridSpace
Decollation allows packing a GridSpace of DynamicMaps into a single DynamicMap
that returns a GridSpace of simple (non-dynamic) elements. All nested streams
are lifted to the resulting DynamicMap, and are available in the `streams`
property. The `callback` property of the resulting DynamicMap is a pure,
stateless function of the stream values. To avoid stream parameter name
conflicts, the resulting DynamicMap is configured with
positional_stream_args=True, and the callback function accepts stream values
as positional dict arguments.
Returns:
DynamicMap that returns a GridSpace
"""
from .decollate import decollate
return decollate(self)


class GridMatrix(GridSpace):
Expand Down

0 comments on commit 17f7727

Please sign in to comment.