-
-
Notifications
You must be signed in to change notification settings - Fork 394
/
spaces.py
713 lines (599 loc) · 27.9 KB
/
spaces.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
from numbers import Number
import numpy as np
import param
import types
from . import traversal, util
from .dimension import OrderedDict, Dimension, Dimensioned, ViewableElement
from .layout import Layout, AdjointLayout, NdLayout
from .ndmapping import UniformNdMapping, NdMapping, item_check
from .overlay import Overlayable, Overlay, CompositeOverlay, NdOverlay
from .tree import AttrTree
class HoloMap(UniformNdMapping):
"""
A HoloMap can hold any number of DataLayers indexed by a list of
dimension values. It also has a number of properties, which can find
the x- and y-dimension limits and labels.
"""
data_type = (ViewableElement, NdMapping, Layout)
def overlay(self, dimensions=None, **kwargs):
"""
Splits the UniformNdMapping along a specified number of dimensions and
overlays items in the split out Maps.
Shows all HoloMap data When no dimensions are specified.
"""
dimensions = self._valid_dimensions(dimensions)
if len(dimensions) == self.ndims:
with item_check(False):
return NdOverlay(self, **kwargs)
else:
dims = [d for d in self.kdims if d not in dimensions]
return self.groupby(dims, group_type=NdOverlay, **kwargs)
def grid(self, dimensions=None, **kwargs):
"""
GridSpace takes a list of one or two dimensions, and lays out the containing
Views along these axes in a GridSpace.
Shows all HoloMap data When no dimensions are specified.
"""
dimensions = self._valid_dimensions(dimensions)
if len(dimensions) == self.ndims:
with item_check(False):
return GridSpace(self, **kwargs)
return self.groupby(dimensions, container_type=GridSpace, **kwargs)
def layout(self, dimensions=None, **kwargs):
"""
GridSpace takes a list of one or two dimensions, and lays out the containing
Views along these axes in a GridSpace.
Shows all HoloMap data When no dimensions are specified.
"""
dimensions = self._valid_dimensions(dimensions)
if len(dimensions) == self.ndims:
with item_check(False):
return NdLayout(self, **kwargs)
return self.groupby(dimensions, container_type=NdLayout, **kwargs)
def split_overlays(self):
"""
Given a UniformNdMapping of Overlays of N layers, split out the layers into
N separate Maps.
"""
if not issubclass(self.type, CompositeOverlay):
return None, self.clone()
item_maps = OrderedDict()
for k, overlay in self.data.items():
for key, el in overlay.items():
if key not in item_maps:
item_maps[key] = [(k, el)]
else:
item_maps[key].append((k, el))
maps, keys = [], []
for k, layermap in item_maps.items():
maps.append(self.clone(layermap))
keys.append(k)
return keys, maps
def _dimension_keys(self):
"""
Helper for __mul__ that returns the list of keys together with
the dimension labels.
"""
return [tuple(zip([d.name for d in self.kdims], [k] if self.ndims == 1 else k))
for k in self.keys()]
def __mul__(self, other):
"""
The mul (*) operator implements overlaying of different Views.
This method tries to intelligently overlay Maps with differing
keys. If the UniformNdMapping is mulled with a simple
ViewableElement each element in the UniformNdMapping is
overlaid with the ViewableElement. If the element the
UniformNdMapping is mulled with is another UniformNdMapping it
will try to match up the dimensions, making sure that items
with completely different dimensions aren't overlaid.
"""
if isinstance(other, self.__class__):
self_set = {d.name for d in self.kdims}
other_set = {d.name for d in other.kdims}
# Determine which is the subset, to generate list of keys and
# dimension labels for the new view
self_in_other = self_set.issubset(other_set)
other_in_self = other_set.issubset(self_set)
dimensions = self.kdims
if self_in_other and other_in_self: # superset of each other
super_keys = sorted(set(self._dimension_keys() + other._dimension_keys()))
elif self_in_other: # self is superset
dimensions = other.kdims
super_keys = other._dimension_keys()
elif other_in_self: # self is superset
super_keys = self._dimension_keys()
else: # neither is superset
raise Exception('One set of keys needs to be a strict subset of the other.')
items = []
for dim_keys in super_keys:
# Generate keys for both subset and superset and sort them by the dimension index.
self_key = tuple(k for p, k in sorted(
[(self.get_dimension_index(dim), v) for dim, v in dim_keys
if dim in self.kdims]))
other_key = tuple(k for p, k in sorted(
[(other.get_dimension_index(dim), v) for dim, v in dim_keys
if dim in other.kdims]))
new_key = self_key if other_in_self else other_key
# Append SheetOverlay of combined items
if (self_key in self) and (other_key in other):
items.append((new_key, self[self_key] * other[other_key]))
elif self_key in self:
items.append((new_key, Overlay([self[self_key]])))
else:
items.append((new_key, Overlay([other[other_key]])))
return self.clone(items, kdims=dimensions, label=self._label, group=self._group)
elif isinstance(other, self.data_type):
items = [(k, v * other) for (k, v) in self.data.items()]
return self.clone(items, label=self._label, group=self._group)
else:
raise Exception("Can only overlay with {data} or {vmap}.".format(
data=self.data_type, vmap=self.__class__.__name__))
def __add__(self, obj):
return Layout.from_values(self) + Layout.from_values(obj)
def __lshift__(self, other):
if isinstance(other, (ViewableElement, UniformNdMapping)):
return AdjointLayout([self, other])
elif isinstance(other, AdjointLayout):
return AdjointLayout(other.data+[self])
else:
raise TypeError('Cannot append {0} to a AdjointLayout'.format(type(other).__name__))
def collate(self, merge_type=None, drop=[], drop_constant=False):
"""
Collation allows collapsing nested HoloMaps by merging
their dimensions. In the simple case a HoloMap containing
other HoloMaps can easily be joined in this way. However
collation is particularly useful when the objects being
joined are deeply nested, e.g. you want to join multiple
Layouts recorded at different times, collation will return
one Layout containing HoloMaps indexed by Time. Changing
the merge_type will allow merging the outer Dimension
into any other UniformNdMapping type.
Specific dimensions may be dropped if they are redundant
by supplying them in a list. Enabling drop_constant allows
ignoring any non-varying dimensions during collation.
"""
from .element import Collator
merge_type=merge_type if merge_type else self.__class__
return Collator(self, merge_type=merge_type, drop=drop,
drop_constant=drop_constant)()
def collapse(self, dimensions=None, function=None, spreadfn=None, **kwargs):
"""
Allows collapsing one of any number of key dimensions
on the HoloMap. Homogenous Elements may be collapsed by
supplying a function, inhomogenous elements are merged.
"""
from .operation import MapOperation
if not dimensions:
dimensions = self.kdims
if self.ndims > 1 and len(dimensions) != self.ndims:
groups = self.groupby([dim for dim in self.kdims
if dim not in dimensions])
else:
[self.get_dimension(dim) for dim in dimensions]
groups = HoloMap([(0, self)])
collapsed = groups.clone(shared_data=False)
for key, group in groups.items():
if isinstance(function, MapOperation):
collapsed[key] = function(group, **kwargs)
else:
group_data = [el.data for el in group]
args = (group_data, function, group.last.kdims)
if hasattr(group.last, 'interface'):
col_data = group.type(group.table().aggregate(group.last.kdims, function, spreadfn, **kwargs))
else:
data = group.type.collapse_data(*args, **kwargs)
col_data = group.last.clone(data)
collapsed[key] = col_data
return collapsed if self.ndims > 1 else collapsed.last
def sample(self, samples=[], bounds=None, **sample_values):
"""
Sample each Element in the UniformNdMapping by passing either a list of
samples or a tuple specifying the number of regularly spaced
samples per dimension. Alternatively, a single sample may be
requested using dimension-value pairs. Optionally, the bounds
argument can be used to specify the bounding extent from which
the coordinates are to regularly sampled. Regular sampling
assumes homogenous and regularly sampled data.
For 1D sampling, the shape is simply as the desired number of
samples (and not a tuple). The bounds format for 1D sampling
is the tuple (lower, upper) and the tuple (left, bottom,
right, top) for 2D sampling.
"""
dims = self.last.ndims
if isinstance(samples, tuple) or np.isscalar(samples):
if dims == 1:
xlim = self.last.range(0)
lower, upper = (xlim[0], xlim[1]) if bounds is None else bounds
edges = np.linspace(lower, upper, samples+1)
linsamples = [(l+u)/2.0 for l,u in zip(edges[:-1], edges[1:])]
elif dims == 2:
(rows, cols) = samples
if bounds:
(l,b,r,t) = bounds
else:
l, r = self.last.range(0)
b, t = self.last.range(1)
xedges = np.linspace(l, r, cols+1)
yedges = np.linspace(b, t, rows+1)
xsamples = [(lx+ux)/2.0 for lx,ux in zip(xedges[:-1], xedges[1:])]
ysamples = [(ly+uy)/2.0 for ly,uy in zip(yedges[:-1], yedges[1:])]
X,Y = np.meshgrid(xsamples, ysamples)
linsamples = zip(X.flat, Y.flat)
else:
raise NotImplementedError("Regular sampling not implented"
"for high-dimensional Views.")
samples = set(self.last.closest(linsamples))
sampled = self.clone([(k, view.sample(samples, **sample_values))
for k, view in self.data.items()])
return sampled.table()
def reduce(self, dimensions=None, function=None, **reduce_map):
"""
Reduce each Element in the HoloMap using a function supplied
via the kwargs, where the keyword has to match a particular
dimension in the Elements.
"""
from ..element import Table
reduced_items = [(k, v.reduce(dimensions, function, **reduce_map))
for k, v in self.items()]
if not isinstance(reduced_items[0][1], Table):
params = dict(util.get_param_values(self.last),
kdims=self.kdims, vdims=self.last.vdims)
return Table(reduced_items, **params)
return self.clone(reduced_items).table()
def relabel(self, label=None, group=None, depth=1):
# Identical to standard relabel method except for default depth of 1
return super(HoloMap, self).relabel(label=label, group=group, depth=depth)
def hist(self, num_bins=20, bin_range=None, adjoin=True, individually=True, **kwargs):
histmaps = [self.clone(shared_data=False)
for d in kwargs.get('dimension', range(1))]
if individually:
map_range = None
else:
if 'dimension' not in kwargs:
raise Exception("Please supply the dimension to compute a histogram for.")
map_range = self.range(kwargs['dimension'])
bin_range = map_range if bin_range is None else bin_range
style_prefix = 'Custom[<' + self.name + '>]_'
if issubclass(self.type, (NdOverlay, Overlay)) and 'index' not in kwargs:
kwargs['index'] = 0
for k, v in self.data.items():
hists = v.hist(adjoin=False, bin_range=bin_range,
individually=individually, num_bins=num_bins,
style_prefix=style_prefix, **kwargs)
if isinstance(hists, Layout):
for i, hist in enumerate(hists):
histmaps[i][k] = hist
else:
histmaps[0][k] = hists
if adjoin:
layout = self
for hist in histmaps:
layout = (layout << hist)
if issubclass(self.type, (NdOverlay, Overlay)):
layout.main_layer = kwargs['index']
return layout
else:
if len(histmaps) > 1:
return Layout.from_values(histmaps)
else:
return histmaps[0]
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 'closed' 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. Closed mode only applied to callables where all the key
dimensions are fully bounded.
"""
_sorted = False
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).
""")
cache_size = param.Integer(default=500, doc="""
The number of entries to cache for fast access. This is an LRU
cache where the least recently used item is overwritten once
the cache is full.""")
cache_interval = param.Integer(default=1, doc="""
When the element counter modulo the cache_interval is zero, the
element will be cached and therefore accessible when casting to a
HoloMap. Applicable in open mode only.""")
def __init__(self, initial_items=None, **params):
super(DynamicMap, self).__init__(initial_items, **params)
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 = 'closed' if self.call_mode == 'key' else 'open'
# Needed to initialize the plotting system
if self.call_mode == 'key':
self[self._initial_key()]
elif self.call_mode == 'counter':
self[self.counter]
self.counter += 1
else:
next(self)
def _initial_key(self):
"""
Construct an initial key for closed mode based on the lower
range bounds or values on the key dimensions.
"""
key = []
for kdim in self.kdims:
if kdim.values:
key.append(kdim.values[0])
elif kdim.range:
key.append(kdim.range[0])
return tuple(key)
def _validate_mode(self):
"""
Check the key dimensions and callback to determine the calling mode.
"""
isgenerator = isinstance(self.callback, types.GeneratorType)
if isgenerator:
return 'generator'
# Any unbounded kdim (any direction) implies open mode
for kdim in self.kdims:
if (kdim.values) and kdim.range != (None,None):
raise Exception('Dimension cannot have both values and ranges.')
elif 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
specified by the corresponding dimension range and soft_range.
"""
key = util.wrap_tuple(key)
assert len(key) == len(self.kdims)
for ind, val in enumerate(key):
kdim = self.kdims[ind]
low, high = util.max_range([kdim.range, kdim.soft_range])
if low is not np.NaN:
if val < low:
raise StopIteration("Key value %s below lower bound %s"
% (val, low))
if high is not np.NaN:
if val > high:
raise StopIteration("Key value %s above upper bound %s"
% (val, high))
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
if self.call_mode == 'generator':
retval = self.callback.next()
else:
retval = self.callback(*args)
if self.call_mode=='key':
return retval
if isinstance(retval, tuple):
self._validate_key(retval[0]) # Validated output key
return retval
else:
self._validate_key((self.counter,))
return (self.counter, retval)
def clone(self, data=None, shared_data=True, *args, **overrides):
"""
Overrides Dimensioned clone to avoid checking items if data
is unchanged.
"""
return super(UniformNdMapping, self).clone(data, shared_data,
*args, **overrides)
def reset(self):
"""
Return a cleared dynamic map with a cleared cached
and a reset counter.
"""
if self.call_mode == 'generator':
raise Exception("Cannot reset generators.")
self.counter = 0
self.data = OrderedDict()
return self
def __getitem__(self, key):
"""
Return an element for any key chosen key (in'closed mode') or
for a previously generated key that is still in the cache
(for one of the 'open' modes)
"""
try:
retval = super(DynamicMap,self).__getitem__(key)
if isinstance(retval, DynamicMap):
return HoloMap(retval)
else:
return retval
except KeyError as e:
if self.mode == 'open' and len(self.data)>0:
raise KeyError(str(e) + " Note: Cannot index outside "
"available cache over an open interval.")
tuple_key = util.wrap_tuple(key)
val = self._execute_callback(*tuple_key)
if self.call_mode == 'counter':
val = val[1]
self._cache(tuple_key, val)
return val
def _cache(self, key, val):
"""
Request that a key/value pair be considered for caching.
"""
if self.mode == 'open' and (self.counter % self.cache_interval)!=0:
return
if len(self) >= self.cache_size:
first_key = self.data.iterkeys().next()
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 == 'closed':
raise Exception("The next() method should only be called in "
"one of the open modes.")
args = () if self.call_mode == 'generator' else (self.counter,)
retval = self._execute_callback(*args)
(key, val) = (retval if isinstance(retval, tuple)
else (self.counter, retval))
key = util.wrap_tuple(key)
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
class GridSpace(UniformNdMapping):
"""
Grids are distinct from Layouts as they ensure all contained
elements to be of the same type. Unlike Layouts, which have
integer keys, Grids usually have floating point keys, which
correspond to a grid sampling in some two-dimensional space. This
two-dimensional space may have to arbitrary dimensions, e.g. for
2D parameter spaces.
"""
kdims = param.List(default=[Dimension(name="X"), Dimension(name="Y")],
bounds=(1,2))
def __init__(self, initial_items=None, **params):
super(GridSpace, self).__init__(initial_items, **params)
if self.ndims > 2:
raise Exception('Grids can have no more than two dimensions.')
def __mul__(self, other):
if isinstance(other, GridSpace):
if set(self.keys()) != set(other.keys()):
raise KeyError("Can only overlay two ParameterGrids if their keys match")
zipped = zip(self.keys(), self.values(), other.values())
overlayed_items = [(k, el1 * el2) for (k, el1, el2) in zipped]
return self.clone(overlayed_items)
elif isinstance(other, UniformNdMapping) and len(other) == 1:
view = other.last
elif isinstance(other, UniformNdMapping) and len(other) != 1:
raise Exception("Can only overlay with HoloMap of length 1")
else:
view = other
overlayed_items = [(k, el * view) for k, el in self.items()]
return self.clone(overlayed_items)
def __lshift__(self, other):
if isinstance(other, (ViewableElement, UniformNdMapping)):
return AdjointLayout([self, other])
elif isinstance(other, AdjointLayout):
return AdjointLayout(other.data+[self])
else:
raise TypeError('Cannot append {0} to a AdjointLayout'.format(type(other).__name__))
def _transform_indices(self, key):
"""
Transforms indices by snapping to the closest value if
values are numeric, otherwise applies no transformation.
"""
ndims = self.ndims
if all(not isinstance(el, slice) for el in key):
dim_inds = []
for dim in self.kdims:
dim_type = self.get_dimension_type(dim)
if isinstance(dim_type, type) and issubclass(dim_type, Number):
dim_inds.append(self.get_dimension_index(dim))
str_keys = iter(key[i] for i in range(self.ndims)
if i not in dim_inds)
num_keys = []
if len(dim_inds):
keys = list({tuple(k[i] if ndims > 1 else k for i in dim_inds)
for k in self.keys()})
q = np.array([tuple(key[i] if ndims > 1 else key for i in dim_inds)])
idx = np.argmin([np.inner(q - np.array(x), q - np.array(x))
if len(dim_inds) == 2 else np.abs(q-x)
for x in keys])
num_keys = iter(keys[idx])
key = tuple(next(num_keys) if i in dim_inds else next(str_keys)
for i in range(self.ndims))
elif any(not isinstance(el, slice) for el in key):
index_inds = [idx for idx, el in enumerate(key)
if not isinstance(el, (slice, str))]
if len(index_inds):
index_ind = index_inds[0]
dim_keys = np.array([k[index_ind] for k in self.keys()])
snapped_val = dim_keys[np.argmin(np.abs(dim_keys-key[index_ind]))]
key = list(key)
key[index_ind] = snapped_val
key = tuple(key)
return key
def keys(self, full_grid=False):
"""
Returns a complete set of keys on a GridSpace, even when GridSpace isn't fully
populated. This makes it easier to identify missing elements in the
GridSpace.
"""
keys = super(GridSpace, self).keys()
if self.ndims == 1 or not full_grid:
return keys
dim1_keys = sorted(set(k[0] for k in keys))
dim2_keys = sorted(set(k[1] for k in keys))
return [(d1, d2) for d1 in dim1_keys for d2 in dim2_keys]
@property
def last(self):
"""
The last of a GridSpace is another GridSpace
constituted of the last of the individual elements. To access
the elements by their X,Y position, either index the position
directly or use the items() method.
"""
if self.type == HoloMap:
last_items = [(k, v.last if isinstance(v, HoloMap) else v)
for (k, v) in self.data.items()]
else:
last_items = self.data
return self.clone(last_items)
def __len__(self):
"""
The maximum depth of all the elements. Matches the semantics
of __len__ used by Maps. For the total number of elements,
count the full set of keys.
"""
return max([(len(v) if hasattr(v, '__len__') else 1) for v in self.values()] + [0])
def __add__(self, obj):
return Layout.from_values(self) + Layout.from_values(obj)
@property
def shape(self):
keys = self.keys()
if self.ndims == 1:
return (len(keys), 1)
return len(set(k[0] for k in keys)), len(set(k[1] for k in keys))
class GridMatrix(GridSpace):
"""
GridMatrix is container type for heterogeneous Element types
laid out in a grid. Unlike a GridSpace the axes of the Grid
must not represent an actual coordinate space, but may be used
to plot various dimensions against each other. The GridMatrix
is usually constructed using the gridmatrix operation, which
will generate a GridMatrix plotting each dimension in an
Element against each other.
"""
def _item_check(self, dim_vals, data):
if not traversal.uniform(NdMapping([(0, self), (1, data)])):
raise ValueError("HoloMaps dimensions must be consistent in %s." %
type(self).__name__)
NdMapping._item_check(self, dim_vals, data)