1+ from collections .abc import Sequence
12from warnings import warn
23
34import h5py
1112)
1213
1314
15+ def _expand_ellipsis (ndim , indexing ):
16+ """Expand numpy indexing to one slice/index per dimension"""
17+ # expanding Ellipsis
18+ if any (x is Ellipsis for x in indexing ):
19+ ellipsis_idx = indexing .index (Ellipsis )
20+ # Count non-Ellipsis
21+ n_before = len (indexing [:ellipsis_idx ])
22+ n_after = len (indexing [ellipsis_idx + 1 :])
23+ n_ellipsis = max (0 , ndim - n_before - n_after )
24+
25+ # Replace Ellipsis with appropriate number of colons
26+ indexing = (indexing [:ellipsis_idx ] +
27+ (slice (None ),) * n_ellipsis +
28+ indexing [ellipsis_idx + 1 :])
29+
30+ # Pad with slice(None) if indexing is shorter than ndim
31+ return indexing + (slice (None ),) * (ndim - len (indexing ))
32+
33+
1434def expand_indexing (shape , indexing ):
1535 """
1636 Expand numpy indexing into explicit coordinate arrays for each dimension.
@@ -32,23 +52,7 @@ def expand_indexing(shape, indexing):
3252 if not isinstance (indexing , tuple ):
3353 indexing = (indexing ,)
3454
35- ndim = len (shape )
36-
37- # expanding Ellipsis
38- if any (x is Ellipsis for x in indexing ):
39- ellipsis_idx = indexing .index (Ellipsis )
40- # Count non-Ellipsis
41- n_before = len (indexing [:ellipsis_idx ])
42- n_after = len (indexing [ellipsis_idx + 1 :])
43- n_ellipsis = max (0 , ndim - n_before - n_after )
44-
45- # Replace Ellipsis with appropriate number of colons
46- indexing = (indexing [:ellipsis_idx ] +
47- (slice (None ),) * n_ellipsis +
48- indexing [ellipsis_idx + 1 :])
49-
50- # Pad with slice(None) if indexing is shorter than ndim
51- indexing += (slice (None ),) * (ndim - len (indexing ))
55+ indexing = _expand_ellipsis (len (shape ), indexing )
5256
5357 # Process each index and expand coordinates
5458 result = []
@@ -104,7 +108,7 @@ class KeyData:
104108 """
105109 def __init__ (
106110 self , source , key , * , train_ids , files , section , dtype , eshape ,
107- inc_suspect_trains = True ,
111+ edims = None , roi = (), inc_suspect_trains = True ,
108112 ):
109113 self .source = source
110114 self .key = key
@@ -114,6 +118,10 @@ def __init__(
114118 self .dtype = dtype
115119 self .entry_shape = eshape
116120 self .ndim = len (eshape ) + 1
121+ if edims is None :
122+ edims = {f"dim_{ i } " : None for i in range (len (eshape ))}
123+ self ._entry_dims = edims
124+ self ._roi = roi
117125 self .inc_suspect_trains = inc_suspect_trains
118126
119127 def _find_chunks (self ):
@@ -274,6 +282,8 @@ def _without_virtual_overview(self):
274282 section = self .section ,
275283 dtype = self .dtype ,
276284 eshape = self .entry_shape ,
285+ edims = self ._entry_dims ,
286+ roi = self ._roi ,
277287 inc_suspect_trains = self .inc_suspect_trains ,
278288 )
279289
@@ -342,6 +352,8 @@ def _only_tids(self, tids, files=None):
342352 section = self .section ,
343353 dtype = self .dtype ,
344354 eshape = self .entry_shape ,
355+ edims = self ._entry_dims ,
356+ roi = self ._roi ,
345357 inc_suspect_trains = self .inc_suspect_trains ,
346358 )
347359
@@ -597,39 +609,43 @@ def xarray(self, extra_dims=None, roi=(), name=None, extra_coords=None):
597609 given, it should map dimension names to coordinate arrays. If True,
598610 default coordinate arrays will be generated.
599611 """
600- import xarray
612+ import xarray # Fail before loading data if xarray is missing
601613
602614 ndarr = self .ndarray (roi = roi )
603615
604- # Train ID index
605- coords = {'trainId' : self .train_id_coordinates ()}
606- dims = ['trainId' ]
607-
608- def _dim_name (idx ):
609- if extra_dims is not None :
610- return extra_dims [idx ]
611- else :
612- return f'dim_{ idx } '
613-
614- # Dimension labels after the train dimension
615- if extra_dims is not None and isinstance (extra_coords , dict ):
616- dims += extra_dims
617- coords |= extra_coords
616+ return self ._wrap_xarray (
617+ ndarr , extra_dims = extra_dims , roi = roi , name = name , extra_coords = extra_coords
618+ )
618619
619- elif isinstance (extra_coords , dict ):
620- coords |= extra_coords
621- dims += ['dim_%d' % i for i in range (ndarr .ndim - 1 )]
620+ def _wrap_xarray (self , arr , extra_dims = None , roi = (), name = None , extra_coords = None ):
621+ import xarray
622622
623- elif extra_coords or extra_dims is not None :
624- # add default coordinates if extra_coords is True or extra_dims given.
625- for idx , coord in enumerate (expand_indexing (self .entry_shape , roi )):
626- dim = _dim_name (idx )
627- coords [dim ] = coord
623+ if (extra_dims is not None ) or (extra_coords is not None ):
624+ if extra_coords is None :
625+ dim_info = extra_dims
626+ elif (extra_dims is None ) and (extra_coords is True ):
627+ # Pass a sequence of names so that with_entry_dims will generate
628+ # integer coordinates.
629+ dim_info = list (self ._entry_dims .keys ())
630+ elif extra_dims is None :
631+ # Allowed to attach coordinates to a subset of existing dimensions
632+ if missing := set (extra_coords ) - set (self ._entry_dims ):
633+ raise ValueError (f"No dimensions named { missing } " )
634+ dim_info = {k : extra_coords .get (k , v )
635+ for (k , v ) in self ._entry_dims .items ()}
636+ else : # Both specified
637+ dim_info = {n : extra_coords .get (n , None ) for n in extra_dims }
638+
639+ return self .with_entry_dims (dim_info , roi = roi )._wrap_xarray (arr , name = name )
640+
641+ roi = _expand_ellipsis (len (self .entry_shape ), self ._roi )
642+ dims = ['trainId' ] + [
643+ n for n , sel in zip (self ._entry_dims , roi ) if not isinstance (sel , int )
644+ ]
628645
629- if not isinstance (coord , int ):
630- dims .append (dim )
631- else :
632- dims += ['dim_%d' % i for i in range (ndarr .ndim - 1 )]
646+ coords = {'trainId' : self .train_id_coordinates ()} | {
647+ k : v for (k , v ) in self ._entry_dims .items () if (v is not None )
648+ }
633649
634650 # xarray attributes
635651 attrs = {}
@@ -639,11 +655,12 @@ def _dim_name(idx):
639655 except Exception as e :
640656 warn (f"Exception fetching units: { e } " )
641657
642- if ndarr .dtype .names is not None :
658+ if arr .dtype .names is not None :
643659 # Structured dtype.
644660 return xarray .Dataset (
645- {field : (dims , ndarr [field ]) for field in ndarr .dtype .names },
646- coords = coords , attrs = attrs )
661+ {field : (dims , arr [field ]) for field in arr .dtype .names },
662+ coords = coords , attrs = attrs
663+ )
647664 else :
648665 if name is None :
649666 name = f'{ self .source } .{ self .key } '
@@ -653,7 +670,8 @@ def _dim_name(idx):
653670
654671 # Primitive dtype.
655672 return xarray .DataArray (
656- ndarr , dims = dims , coords = coords , name = name , attrs = attrs )
673+ arr , dims = dims , coords = coords , name = name , attrs = attrs
674+ )
657675
658676 def series (self ):
659677 """Load this data as a pandas Series. Only for 1D data.
@@ -671,7 +689,8 @@ def series(self):
671689 data = self .ndarray ()
672690 return pd .Series (data , name = name , index = index )
673691
674- def dask_array (self , labelled = False ):
692+ def dask_array (self , labelled = False , * , roi = (), name = None , extra_dims = None ,
693+ extra_coords = None ):
675694 """Make a Dask array for this data.
676695
677696 Dask is a system for lazy parallel computation. This method doesn't
@@ -691,6 +710,26 @@ def dask_array(self, labelled=False):
691710 labelled: bool
692711 If True, label the train IDs for the data, returning an
693712 xarray.DataArray object wrapping a Dask array.
713+ roi: numpy.s_[], slice, or tuple of slices
714+ The region of interest. This expression selects data in all
715+ dimensions apart from the first (trains) dimension. If the data
716+ holds a 1D array for each entry, roi=np.s_[:8] would get the first 8
717+ values from every train. If the data is 2D or more at each entry,
718+ selection looks like roi=np.s_[:8, 5:10] .
719+ name: str
720+ Name the array itself. The default is the source and key joined by a
721+ dot. Ignored if labelled is False, and for structured data when a
722+ dataset is returned.
723+ extra_dims: list of str
724+ Name extra dimensions in the array. The first dimension is
725+ automatically called 'train'. The default for extra dimensions is
726+ dim_0, dim_1, ... Ignored if labelled is False.
727+ extra_coords: bool or dict
728+ Add coordinates to the returned DataArray. If roi is used, the
729+ coordinates will match the selected region of interest. If a dict is
730+ given, it should map dimension names to coordinate arrays. If True,
731+ default coordinate arrays will be generated. Ignored if labelled is
732+ False.
694733 """
695734 import dask .array as da
696735
@@ -725,17 +764,79 @@ def dask_array(self, labelled=False):
725764 shape = (0 ,) + self .entry_shape
726765 dask_arr = da .zeros (shape = shape , dtype = self .dtype , chunks = shape )
727766
767+ dask_arr = dask_arr [:, * roi ]
768+
728769 if labelled :
729- # Dimension labels
730- dims = ['trainId' ] + ['dim_%d' % i for i in range (dask_arr .ndim - 1 )]
770+ return self ._wrap_xarray (
771+ dask_arr ,
772+ extra_dims = extra_dims ,
773+ roi = roi , name = name ,
774+ extra_coords = extra_coords
775+ )
776+ else :
777+ return dask_arr
778+
779+ def with_entry_dims (self , dims = None , roi = None , ** kwargs ):
780+ """Attach dimension names, ROI and optional coordinate labels
731781
732- # Train ID index
733- coords = { 'trainId' : self . train_id_coordinates ()}
782+ Parameters
783+ ----------
734784
735- import xarray
736- return xarray .DataArray (dask_arr , dims = dims , coords = coords )
785+ dims: list or dict
786+ Either a list of dimension names, or a dictionary mapping dimension
787+ names to coordinate labels. Every dimension except for the trains/
788+ entries dimension must be named, but coordinate labels are optional.
789+ Uses None in the dict format to omit coordinate labels. If a ROI
790+ is specified, coordinate labels should be only for that selection.
791+ roi: numpy.s_[], slice, or tuple of slices
792+ The region of interest. This expression selects data in all
793+ dimensions apart from the first (trains) dimension. If the data
794+ holds a 1D array for each entry, roi=np.s_[:8] would get the first 8
795+ values from every train. If the data is 2D or more at each entry,
796+ selection looks like roi=np.s_[:8, 5:10] .
797+ kwargs:
798+ The dictionary format for dims can be passed as keyword arguments
799+ instead. Don't mix keyword arguments with a dims list/dict.
800+ """
801+ if not isinstance (roi , tuple ):
802+ roi = roi ,
803+
804+ if dims is None :
805+ if not kwargs :
806+ raise TypeError ("No dimension information specified" )
807+ coords = kwargs
808+ elif kwargs :
809+ raise TypeError ("Mixing positional and keyword arguments is not supported" )
810+ elif isinstance (dims , str ):
811+ coords = {dims : None }
812+ elif isinstance (dims , Sequence ):
813+ # If we are given names, generate integer indexes
814+ coords = {name : coord for (name , coord ) in
815+ zip (dims , expand_indexing (self .entry_shape , roi ))}
816+ elif isinstance (dims , dict ):
817+ coords = dims
737818 else :
738- return dask_arr
819+ raise TypeError (
820+ f"Unexpected type for dimension/coordinate info: { type (dims )} "
821+ )
822+
823+ if len (coords ) != len (self .entry_shape ):
824+ raise TypeError (
825+ f"Expected names for { len (self .entry_shape )} dimensions, got { len (coords )} "
826+ )
827+
828+ return KeyData (
829+ self .source , self .key ,
830+ train_ids = self .train_ids ,
831+ files = self .files ,
832+ section = self .section ,
833+ dtype = self .dtype ,
834+ eshape = self .entry_shape ,
835+ edims = coords ,
836+ roi = roi ,
837+ inc_suspect_trains = self .inc_suspect_trains ,
838+ )
839+
739840
740841 # Getting data by train: --------------------------------------------------
741842
0 commit comments