Home > Software design >  What is a strategy to properly shape arrays and vectors to avoid "could not be broadcast togeth
What is a strategy to properly shape arrays and vectors to avoid "could not be broadcast togeth

Time:11-11

This has happened to me several times with different equations, and right now, I struggle with it again. I am sure others have the same problem and look for a memorable strategy to shape arrays and vectors so that non-trivial NumPy computations go through without fiddling and poking.

This is the equation I want to implement: enter image description here, where k is a scalar, and n, a, R and R0 are 3D vectors. R0 is a NumPy array holding four 3D vectors.

Now I get stuck in my workflow because I try to find a way to pierce together the NumPy calculation. I want to use NumPy's awesome parallelization for math and not break up the four 3D vectors and do a for-loop. I am not good enough at NumPy to "see" the shapes of intermediate result arrays. And so I end up with try-and-error, which gives ugly results and is unsatisfying.

Please don't just give a working solution but explain why you did what you did.

Here is my current code - which does not work as it should (the transposing of random arrays should not be there).

import numpy as np

def main():
    r_0 = np.array([[.265, 0, .382],
                    [0, .712, .764],
                    [0, .712, 0],
                    [.752, .712, .382]])

    b_measured = np.array([[1.64712096, 4.87716371, -0.77060129],
                           [1.55980676, 4.93977942, -0.7133636],
                           [1.40883668, 4.96624651, -0.71742247],
                           [1.6531721, 5.02004066, -0.72243437]])

    n = np.array([0, 0, 1])
    a = np.array([.2, 0, 0])
    r = np.array([0, 2, 0])

    r_relativ = (r - r_0)
    r_magnitude = np.linalg.norm(r_relativ, axis=1)
    error = -np.cross(a - 2 * (r_relativ.T * np.matmul(r_relativ, a)).T, n,
                      axisa=1) / r_magnitude ** 4 - b_measured
    print(error)


if '__name__' == '__main__':
    main()

CodePudding user response:

This is a casting problem from numpy. In this line:

r_magnitude = np.linalg.norm(r_relativ, axis=1)

you are getting a (4,) shape, which is a vector, not an array. You can check that by adding a

print(r_magnitude.shape)

This generates difficulties with numpy casting, as numpy doesn't quite know how to cast this second undefined dimension. That is why you probably get the error

ValueError: operands could not be broadcast together with shapes (4,3) (4,) 

Which means, you are trying to broadcast your vector r_magnitude (shape = (4,)) into a (4,3) shaped numpy array. To understand why this happens, you can check this link to Tutorialspoint. Towards the end they explain it.

What I believe is the standard, is to use the keepdims kwarg for basically every numpy method.

r_magnitude = np.linalg.norm(r_relativ, axis=1, keepdims=True)

This will make the operation keep the same number of dimentions as the original array (2 in this case), that is, r_magnitude's shape is now (4,1), making the broadcasting possible

CodePudding user response:

Keeping track of shapes is a large part of numpy coding (and before that MATLAB).

First, I like to have a clear idea of the shapes of the inputs:

In [1]:     r_0 = np.array([[.265, 0, .382],
   ...:                     [0, .712, .764],
   ...:                     [0, .712, 0],
   ...:                     [.752, .712, .382]])
   ...: 
   ...:     b_measured = np.array([[1.64712096, 4.87716371, -0.77060129],
   ...:                            [1.55980676, 4.93977942, -0.7133636],
   ...:                            [1.40883668, 4.96624651, -0.71742247],
   ...:                            [1.6531721, 5.02004066, -0.72243437]])
   ...: 
   ...:     n = np.array([0, 0, 1])
   ...:     a = np.array([.2, 0, 0])
   ...:     r = np.array([0, 2, 0])
   ...: 
In [2]: r_0.shape
Out[2]: (4, 3)
In [3]: b_measured.shape
Out[3]: (4, 3)

The rest are (3,)

While one can use the debugger, I prefer to test code in an interactive session like ipython.

In [4]: r_relativ = (r - r_0)     # this was a (4,3) broadcasting with (3,)
In [5]: r_relativ.shape
Out[5]: (4, 3)

norm with axis, is like sum, mean etc, a reduction function, removing a dimension:

In [6]: r_magnitude = np.linalg.norm(r_relativ, axis=1)
In [7]: r_magnitude.shape
Out[7]: (4,)

though it is possible retain that dimension as a broadcastable size 1:

In [8]: np.linalg.norm(r_relativ, axis=1, keepdims=True).shape
Out[8]: (4, 1)

Your next line is complex, and the error doesn't identify the operator:

In [9]: error = -np.cross(a - 2 * (r_relativ.T * np.matmul(r_relativ, a)).T, n,
   ...:                       axisa=1) / r_magnitude ** 4 - b_measured
Traceback (most recent call last):
  File "<ipython-input-9-8d7646046c43>", line 1, in <module>
    error = -np.cross(a - 2 * (r_relativ.T * np.matmul(r_relativ, a)).T, n,
ValueError: operands could not be broadcast together with shapes (4,3) (4,) 

Looking at the individual pieces:

In [10]: np.matmul(r_relativ, a).shape
Out[10]: (4,)

This is a (4,3) with (3,) producing a (4,). (Also matmul issues different error messages).

In [11]: (r_relativ.T * np.matmul(r_relativ, a)).T.shape
Out[11]: (4, 3)

The cross argument looks ok. cross itself runs. I don't use it enough to remember the exact axis arguments, though the basic operation is for a pair of (N,3) arrays

In [13]: np.cross(a - 2 * (r_relativ.T * np.matmul(r_relativ, a)).T, n,
    ...:                       axisa=1).shape
Out[13]: (4, 3)

Finally, here's the error:

In [15]: np.cross(a - 2 * (r_relativ.T * np.matmul(r_relativ, a)).T, n,
    ...:                       axisa=1)/r_magnitude**4
Traceback (most recent call last):
  File "<ipython-input-15-c4bed1fd9807>", line 1, in <module>
    np.cross(a - 2 * (r_relativ.T * np.matmul(r_relativ, a)).T, n,
ValueError: operands could not be broadcast together with shapes (4,3) (4,) 

Why? As noted the cross produces (4,3). But r_magnitude is (4,). It should be (4,1) to work with the (4,3).

In sum, I keep the 2 broadcasting rules upper most - it can add leading dimensions, and size 1 dimensions can be scaled to match.

The other issue is keeping track of how reductions like norm change the dimensions. keepdims can be a big help here, though it's always possible to use None to add trailing dimensions.

I don't think there's any short cut to being pedantic about the dimensions. Details matter.

With the (4,) correction to (4,1), error runs:

In [16]: r_magnitude = np.linalg.norm(r_relativ, axis=1, keepdims=True)
In [17]: error = -np.cross(a - 2 * (r_relativ.T * np.matmul(r_relativ, a)).T, n,
    ...: 
    ...:                       axisa=1) / r_magnitude ** 4 - b_measured
In [18]: error.shape
Out[18]: (4, 3)
  • Related