You can use np.rollaxis to bring the last axis to the forefront, helping us slice 4 arrays without actually separating them. We perform the required operations and, finally, send the first axis to the end so that the shape of the output array is the same as the inputs. So we would have a solution for generic n-dimensional ndarrays, for example:
def quat_multiply_ndim(quaternion0, quaternion1): x0, y0, z0, w0 = np.rollaxis(quaternion0, -1, 0) x1, y1, z1, w1 = np.rollaxis(quaternion1, -1, 0) result = np.array(( x1*w0 + y1*z0 - z1*y0 + w1*x0, -x1*z0 + y1*w0 + z1*x0 + w1*y0, x1*y0 - y1*x0 + z1*w0 + w1*z0, -x1*x0 - y1*y0 - z1*z0 + w1*w0), dtype=np.float64) return np.rollaxis(result,0, result.ndim)
Run Example -
In [107]: # N-dim arrays ...: a1 = np.random.randint(0,9,(2,3,2,4)) ...: b1 = np.random.randint(0,9,(2,3,2,4)) ...: In [108]: quat_multiply_ndim(a1,b1) # New ndim approach Out[108]: array([[[[ 154., 48., 55., -57.], [ 31., 81., 29., -95.]], [[ 31., 14., 88., 12.], [ 3., 30., 20., -51.]], [[ 104., 61., 102., -39.], [ 0., 14., 14., -56.]]], [[[ -28., 36., 24., -8.], [ 11., 76., -7., -36.]], [[ 54., 3., -2., -19.], [ 52., 62., 15., -55.]], [[ 76., 28., 28., -60.], <--------| [ 14., 54., 13., 5.]]]]) | | In [109]: quat_multiply(a1[1,2],b1[1,2]) # Old 2D approach Out[109]: | array([[ 76., 28., 28., -60.], ------------------| [ 14., 54., 13., 5.]])