Repository: scipy/scipy

Issue:
Storing Rotation object in NumPy array returns an array with 32 dimensions
<!-- 
Thank you for taking the time to file a bug report. 
Please fill in the fields below, deleting the sections that 
don't apply to your issue. You can view the final output
by clicking the preview button above.
Note: This is a comment, and won't appear in the output.
-->

Storing Rotation objects in numpy arrays results in a numpy array with 32 dimensions.

#### Reproducing code example:
<!-- 
If you place your code between the triple backticks below, 
it will be rendered as a code block. 
-->

```
import numpy as np

from scipy.spatial.transform import Rotation as R

r1 = R.identity()
print(r1.as_euler("ZYX"))
arr = np.array([r1])
print(arr)
print(arr.shape)
print(len(arr.shape))
```

#### Error message:
<!-- If any, paste the *full* error message inside a code block
as above (starting from line Traceback)
-->

```
[0. 0. 0.]
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[<scipy.spatial.transform.rotation.Rotation object at 0x7eff21667ac0>]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
32

```

#### Scipy/Numpy/Python version information:
<!-- You can simply run the following and paste the result in a code block
```
import sys, scipy, numpy; print(scipy.__version__, numpy.__version__, sys.version_info)
```
-->

```
1.4.1 1.18.4 sys.version_info(major=3, minor=8, micro=2, releaselevel='final', serial=0)
```

I'm not sure it's a good idea to store Rotation objects in arrays, since they can already store multiple rotations, but this behavior is very unintuitive and does not seem correct. My guess it that there is some maximum depth of 32 dimensions being hit. Perhaps this is a bug in the np.array constructor?


Discussion:
Well, I don't know if this is a bug, but here's what's going on.

The first argument of `np.array` should be, according to the docs, "an array, any object exposing the array interface, an object whose `__array__` method returns an array, or any (nested) sequence". 

The `Rotation` is object is obviously not an array in the `numpy` sense, nor does it have an `__array__` method. 

Unfortunately, it does have the `__len__` and `__getitem__` methods, which, according to [this SO answer](https://stackoverflow.com/questions/43566044/what-is-pythons-sequence-protocol) are enough to call the `Rotation` object a sequence.

What do these functions return? For your example:
```python
>>> len(r1)
1
>>> r1[0]
<scipy.spatial.transform.rotation.Rotation at 0x11b212c40>
```

So the only "item" in `r1` is itself!

Now, recall how the documentation for `np.array` says it can act on *nested* sequences? Numpy first checks how long `r1` is (it has 1 element). Then it checks if `r1` is nested, by checking how long `r1[0]` is. But `r1[0]` is `r1` itself. And so it goes down the rabbit hole, until, like you said, the  maximum dimension limit (32) is reached.

As I'm finishing typing this, I don't think `numpy` is to blame -- but I'm also new here. 

I think the self-reference is a problem. The Rotation object is trying to be both a single rotation and a sequence of rotations at the same time. I actually didn't know this object supported multiple rotations until I encountered this issue, though I'm sure there are benefits of this that I'm not aware of.

Is there a way to fix this without creating two different objects (e.g. a RotationArray) and still supporting rotation sequences?
This could be fixed by adding an `__array__` method and making it raise a `TypeError`.
I'll try to fix it.

Resolving patch:
diff --git a/scipy/spatial/transform/_rotation_spline.py b/scipy/spatial/transform/_rotation_spline.py
--- a/scipy/spatial/transform/_rotation_spline.py
+++ b/scipy/spatial/transform/_rotation_spline.py
@@ -363,6 +363,9 @@ def _solve_for_angular_rates(self, dt, angular_rates, rotvecs):
     def __init__(self, times, rotations):
         from scipy.interpolate import PPoly
 
+        if rotations.single:
+            raise ValueError("`rotations` must be a sequence of rotations.")
+
         if len(rotations) == 1:
             raise ValueError("`rotations` must contain at least 2 rotations.")
 


Test patch:
diff --git a/scipy/spatial/transform/tests/test_rotation.py b/scipy/spatial/transform/tests/test_rotation.py
--- a/scipy/spatial/transform/tests/test_rotation.py
+++ b/scipy/spatial/transform/tests/test_rotation.py
@@ -874,8 +874,6 @@ def test_n_rotations():
     r = Rotation.from_matrix(mat)
 
     assert_equal(len(r), 2)
-    assert_equal(len(r[0]), 1)
-    assert_equal(len(r[1]), 1)
     assert_equal(len(r[:-1]), 1)
 
 
@@ -1043,7 +1041,7 @@ def test_slerp():
 
 
 def test_slerp_single_rot():
-    with pytest.raises(ValueError, match="at least 2 rotations"):
+    with pytest.raises(ValueError, match="must be a sequence of rotations"):
         r = Rotation.from_quat([1, 2, 3, 4])
         Slerp([1], r)
 
@@ -1126,3 +1124,33 @@ def test_multiplication_stability():
     for q in qs:
         rs *= q * rs
         assert_allclose(np.linalg.norm(rs.as_quat(), axis=1), 1)
+
+
+def test_rotation_within_numpy_array():
+    single = Rotation.random()
+    multiple = Rotation.random(2)
+
+    array = np.array(single)
+    assert_equal(array.shape, ())
+
+    array = np.array(multiple)
+    assert_equal(array.shape, (2,))
+    assert_allclose(array[0].as_matrix(), multiple[0].as_matrix())
+    assert_allclose(array[1].as_matrix(), multiple[1].as_matrix())
+
+    array = np.array([single])
+    assert_equal(array.shape, (1,))
+    assert_equal(array[0], single)
+
+    array = np.array([multiple])
+    assert_equal(array.shape, (1, 2))
+    assert_allclose(array[0, 0].as_matrix(), multiple[0].as_matrix())
+    assert_allclose(array[0, 1].as_matrix(), multiple[1].as_matrix())
+
+    array = np.array([single, multiple], dtype=object)
+    assert_equal(array.shape, (2,))
+    assert_equal(array[0], single)
+    assert_equal(array[1], multiple)
+
+    array = np.array([multiple, multiple, multiple])
+    assert_equal(array.shape, (3, 2))