I brought up a while ago about how it would be nice if numpy arrays could have 
their axes 'labeled'.    = I got an implementation that works pretty well for 
me and in the process learned quite a few things, and was hoping to foster some 
more discussion on this topic, as I think I have found a simple/flexible 
solution to support this at the numpy level.

Here are *some* examples code snippets from my unit tests on 'Array':

    a = Array((4,5,6))

    # you can assign data to all axes by attribute:
    a.Axes.Label = (0:'z',1:'y',2:'x'}
    
    # or add metadata to each individually:
    a.Axes[1].Vector = [0,1,0]
    a.Axes[2].Vector = [0,0,1]
    
    # simple case int indexing
    b = a[0]
    assert b.shape == (5,6)
    assert b.Axes.Label == {0:'y',1:'x'}
    assert b.Axes.Vector == {0:[0,1,0],1:[0,0,1]}

    # indexing with slices
    b = a[:,0,:]
    assert b.shape == (4,6)
    assert b.Axes.Label == {0:'z',1:'x'}
    assert b.Axes.Vector == {1:[0,0,1]}
    
    # indexing with ellipsis
    b = a[...,0]
    assert b.shape == (4,5)
    assert b.Axes.Label == {0:'z',1:'y'}
    
    # indexing with ellipsis, newaxis, etc.
    b = a[newaxis,...,2,newaxis]
    assert b.shape == (1,4,5,1)
    assert b.Axes.Label == {1:'z',2:'y'}

    # indexing with lists
    b = a[[1,2],:,[1,2]]
    assert b.shape == (2,5)
    assert b.Axes.Label == {0:'z',1:'y'}
    
    # most interesting examples, indexing with axes labels---------------- 
    # I was a bit confused about how to handle indexing with mixed 
axes/non-axes indexes
    # IE: what does a['x',2:4]  mean?  on what axis is the 2:4 slice being 
applied, the first? the first after 'x'?
    #       One option is to disallow mixing (simpler to implement, understand?)
    #       Instead I chose to treat the axis indexing as a forced assignment 
of an axis to a position.
    
    # axis indexing that transposes the first two dimensions, but otherwise 
does nothing
    b = a['y','z']
    assert b.shape == (5,4,6)
    assert b.Axes.Label == {0:'y',1:'z',2:'x'}
    
    # abusing slices to allow specifying indexes for axes 
    b = a['y':0,'z']
    assert b.shape == (4,6)
    assert b.Axes.Label == {0:'z',1:'x'}
    
    # unfortunately that means a slice index on an axis must be written like so:
    b = a['y':slice(0,2),'x','z']
    assert b.shape == (2,6,4)
    assert b.Axes.Label == {0:'y',1:'x',2:'z'}

    b = a['y':[1,2,3],'x','z':slice(0:1)]
    # or due to the forced transposition, this is the same as:
    c = a['y','x','z'][[1,2,3],:,0:1]

    assert b.shape == (3,6,1)
    assert b.Axes.Label == {0:'y',1:'x',2:'z'}
    assert b.shape == c.shape
    assert b.Axes == c.Axes

    
#----------------------------------------------------------------------------------------


To do all this I essentially had to recreate the effects of numpy indexing on 
axes....  This is not ideal, but so far I seem to have addressed most of the 
indexing I've used, at least. Here is what __getitem__ looks like:

    def __getitem__(self,idxs):
        filtered_idxs,transposed_axes,kept_axes = self.idx_axes(idxs)
        array = self.view(ndarray).transpose(transposed_axes)
        array = array[filtered_idxs]
        if isinstance(array,ndarray):
            array = array.view(Array)
            array.Axes = self.Axes.keep(kept_axes)
        return array  

As you can see idx_axes() essentially recreates a lot of ndarray indexing 
behavior, so that its effects can be explicitly handled.

Having done all this, I think the best way for numpy to support 'labeled' axes 
in the future is by having numpy itself keep track of a very simple tuple 
attribute, like shape, and leave more complex axis naming/labeling to 
subclasses on the python side.  As an example, upon creating a new dimension in 
an array, numpy assigns that dimension a semi-unique id, and this tuple could 
be used in __array_finalize__.

For example my __array_finalize__ could look like:

def __array_finalize__(self,obj):
    if hasattr(obj,'axesdata'):
         for axesid in self.axes:
              if axesid in obj.axes:
                   self.axesdata[axesid] = obj.axesdata[axesid]


This would cover a lot more situations and lead to much simpler code since the 
work required on the C side would be minimal, but still allow robust and custom 
tracking and propagation of axes information.
Subclasses that tap into this data would react to the result of numpy 
operations vs. having to predict/anticipate.

For example, my __getitem__, relying on the __array_finalize__ above, could 
look like:
     
    def __getitem__(self,idxs):
        filtered_idxs,transposed_axes= self.idx_axes(idxs)
        array = self.transpose(transposed_axes)
        return array[filtered_idxs]

Not shown is how much simpler and robust the code for idx_axes would then be.  
I estimate it would go from 130 loc to < 20 loc.

Sorry for the extra long e-mail,
-Craig


_______________________________________________
NumPy-Discussion mailing list
[email protected]
http://mail.scipy.org/mailman/listinfo/numpy-discussion

Reply via email to