6

With using python library numpy, it is possible to use the function cumprod to evaluate cumulative products, e.g.

a = np.array([1,2,3,4,2])
np.cumprod(a)

gives

array([ 1,  2,  6, 24, 48])

It is indeed possible to apply this function only along one axis.

I would like to do the same with matrices (represented as numpy arrays), e.g. if I have

S0 = np.array([[1, 0], [0, 1]])
Sx = np.array([[0, 1], [1, 0]])
Sy = np.array([[0, -1j], [1j, 0]])
Sz = np.array([[1, 0], [0, -1]])

and

b = np.array([S0, Sx, Sy, Sz])

then I would like to have a cumprod-like function which gives

np.array([S0, S0.dot(Sx), S0.dot(Sx).dot(Sy), S0.dot(Sx).dot(Sy).dot(Sz)])

(This is a simple example, in reality I have potentially large matrices evaluated over n-dimensional meshgrids, so I seek for the most simple and efficient way to evaluate this thing.)

In e.g. Mathematica I would use

FoldList[Dot, IdentityMatrix[2], {S0, Sx, Sy, Sz}]

so I searched for a fold function, and all I found is an accumulate method on numpy.ufuncs. To be honest, I know that I am probably doomed because an attempt at

np.core.umath_tests.matrix_multiply.accumulate(np.array([pauli_0, pauli_x, pauli_y, pauli_z]))

as mentioned in a numpy mailing list yields the error

Reduction not defined on ufunc with signature

Do you have an idea how to (efficiently) do this kind of calculation ?

Thanks in advance.

Georg Sievelson
  • 300
  • 3
  • 7
  • Does this do what you want? `itertools.accumulate(b, func=lambda x, y: x.dot(y))`? You'll need to import the `itertools` module. – Rufflewind Jan 16 '15 at 21:39
  • Yes, it would work if I didn't had to carry out the accumulation along some axis of a multidimensional array. – Georg Sievelson Jan 16 '15 at 21:49
  • 3
    If your matrices are large but the *number* of matrices is small, I might simply write a function with a loop and get on with your day; it takes me ~200 us to take the dot product of two 100x100 float matrices and only 1 us to execute `for i in range(100): pass`, so it's entirely possible that the function overhead will be marginal. – DSM Jan 16 '15 at 21:53
  • @GeorgSievelson: try using it in conjunction to [`apply_along_axis`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.apply_along_axis.html). It's possible that there isn't a native function to do this in numpy, but you may have to ask on the devs to be certain. – Rufflewind Jan 16 '15 at 21:55
  • @DSM I have a lot of matrices (which I hope to keep somehow small), but I begin to fear that writing a loop will be the only sensible way to get things done. Thanks. – Georg Sievelson Jan 16 '15 at 22:11
  • An earlier post in that mailing list thread cites a Cookbook page: http://wiki.scipy.org/Cookbook/MultiDot. It's not new, and focuses on variations of the Python loop. – hpaulj Jan 17 '15 at 00:26
  • The Python bosses have approved a new binary operator, `@`, which might be used in `numpy` as an infix `dot`, allowing `S0 @ Sx @ Sy @ Sz`. Not that it would speed up looping through a long string of these. – hpaulj Jan 17 '15 at 00:28
  • I've sketched out a possible `Cython` solution based on some work I did for `einsum`. – hpaulj Jan 18 '15 at 02:19

1 Answers1

3

As food for thought, here are 3 ways of evaluating the 3 sequential dot products:

With the normal Python reduce (which could also be written as a loop)

In [118]: reduce(np.dot,[S0,Sx,Sy,Sz])
array([[ 0.+1.j,  0.+0.j],
       [ 0.+0.j,  0.+1.j]])

The einsum equivalent

In [119]: np.einsum('ij,jk,kl,lm',S0,Sx,Sy,Sz)

The einsum index expression looks like a sequence of operations, but it is actually evaluated as a 5d product with summation on 3 axes. In the C code this is done with an nditer and strides, but the effect is as follows:

In [120]: np.sum(S0[:,:,None,None,None] * Sx[None,:,:,None,None] *
    Sy[None,None,:,:,None] * Sz[None,None,None,:,:],(1,2,3))

In [127]: np.prod([S0[:,:,None,None,None], Sx[None,:,:,None,None],
    Sy[None,None,:,:,None], Sz[None,None,None,:,:]]).sum((1,2,3))

A while back while creating a patch from np.einsum I translated that C code to Python, and also wrote a Cython sum-of-products function(s). This code is on github at

https://github.com/hpaulj/numpy-einsum

einsum_py.py is the Python einsum, with some useful debugging output

sop.pyx is the Cython code, which is compiled to sop.so.

Here's how it could be used for part of your problem. I'm skipping the Sy array since my sop is not coded for complex numbers (but that could be changed).

import numpy as np
import sop
import einsum_py    

S0 = np.array([[1., 0], [0, 1]])
Sx = np.array([[0., 1], [1, 0]])
Sz = np.array([[1., 0], [0, -1]])

print np.einsum('ij,jk,kl', S0, Sx, Sz)
# [[ 0. -1.] [ 1.  0.]]
# same thing, but with parsing information
einsum_py.myeinsum('ij,jk,kl', S0, Sx, Sz, debug=True)
"""
{'max_label': 108, 'min_label': 105, 'nop': 3, 
 'shapes': [(2, 2), (2, 2), (2, 2)], 
 'strides': [(16, 8), (16, 8), (16, 8)], 
 'ndim_broadcast': 0, 'ndims': [2, 2, 2], 'num_labels': 4,
 ....
 op_axes [[0, -1, 1, -1], [-1, -1, 0, 1], [-1, 1, -1, 0], [0, 1, -1, -1]]
"""    

# take op_axes (for np.nditer) from this debug output
op_axes = [[0, -1, 1, -1], [-1, -1, 0, 1], [-1, 1, -1, 0], [0, 1, -1, -1]]
w = sop.sum_product_cy3([S0,Sx,Sz], op_axes)
print w

As written sum_product_cy3 cannot take an arbitrary number of ops. Plus the iteration space increases with each op and index. But I can imagine calling it repeatedly, either at the Cython level, or from Python. I think it has potential for being faster than repeat(dot...) for lots of small arrays.

A condensed version of the Cython code is:

def sum_product_cy3(ops, op_axes, order='K'):
    #(arr, axis=None, out=None):
    cdef np.ndarray[double] x, y, z, w
    cdef int size, nop
    nop = len(ops)
    ops.append(None)
    flags = ['reduce_ok','buffered', 'external_loop'...]
    op_flags = [['readonly']]*nop + [['allocate','readwrite']]

    it = np.nditer(ops, flags, op_flags, op_axes=op_axes, order=order)
    it.operands[nop][...] = 0
    it.reset()
    for x, y, z, w in it:
        for i in range(x.shape[0]):
           w[i] = w[i] + x[i] * y[i] * z[i]
    return it.operands[nop]
hpaulj
  • 221,503
  • 14
  • 230
  • 353