Repository: scipy/scipy

Issue:
scipy.spatial.transform.Rotation multiplication does not normalize quaternion
<!-- 
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.
-->

Not sure if this is considered a "bug" or not, but when rotations are multiplied in scipy.spatial.transform.Rotation, the resulting quaternion is not normalized.  This can eventually lead to numerical precision errors (e.g. a quaternion that is all NaNs or all zeros), in relatively few rotation iterations (in this example fewer than 100).

#### 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

trialEulerAngles = np.array(
    [
        np.linspace(0,np.pi/6,100),
        np.linspace(0,np.pi/5,100),
        np.linspace(0,-np.pi/8,100)
    ]
).T
r1 = Rotation.from_euler('xyz',[0,0,0])

for eulerAngle in trialEulerAngles:
    r2 = Rotation.from_euler('xyz',eulerAngle)
    dr = r2 * r1.inv()
    r1=dr*r1

print(r1.as_quat()) # Results in [nan nan nan nan]
```

#### Error message:
<!-- If any, paste the *full* error message inside a code block
as above (starting from line Traceback)
-->

```
Traceback (most recent call last):
C:\Users\jrunnels\AppData\Local\Continuum\anaconda3\lib\site-packages\scipy\spatial\transform\rotation.py:154: RuntimeWarning: overflow encountered in multiply
  product[:, 3] = p[:, 3] * q[:, 3] - np.sum(p[:, :3] * q[:, :3], axis=1)
C:\Users\jrunnels\AppData\Local\Continuum\anaconda3\lib\site-packages\scipy\spatial\transform\rotation.py:154: RuntimeWarning: invalid value encountered in subtract
  product[:, 3] = p[:, 3] * q[:, 3] - np.sum(p[:, :3] * q[:, :3], axis=1)
C:\Users\jrunnels\AppData\Local\Continuum\anaconda3\lib\site-packages\scipy\spatial\transform\rotation.py:155: RuntimeWarning: overflow encountered in multiply
  product[:, :3] = (p[:, None, 3] * q[:, :3] + q[:, None, 3] * p[:, :3] +
C:\Users\jrunnels\AppData\Local\Continuum\anaconda3\lib\site-packages\numpy\core\numeric.py:1876: RuntimeWarning: overflow encountered in multiply
  multiply(a1, b2, out=cp0)
C:\Users\jrunnels\AppData\Local\Continuum\anaconda3\lib\site-packages\numpy\core\numeric.py:1877: RuntimeWarning: overflow encountered in multiply
  tmp = array(a2 * b1)
C:\Users\jrunnels\AppData\Local\Continuum\anaconda3\lib\site-packages\numpy\core\numeric.py:1878: RuntimeWarning: invalid value encountered in subtract
  cp0 -= tmp
C:\Users\jrunnels\AppData\Local\Continuum\anaconda3\lib\site-packages\numpy\core\numeric.py:1879: RuntimeWarning: overflow encountered in multiply
  multiply(a2, b0, out=cp1)
C:\Users\jrunnels\AppData\Local\Continuum\anaconda3\lib\site-packages\numpy\core\numeric.py:1880: RuntimeWarning: overflow encountered in multiply
  multiply(a0, b2, out=tmp)
C:\Users\jrunnels\AppData\Local\Continuum\anaconda3\lib\site-packages\numpy\core\numeric.py:1881: RuntimeWarning: invalid value encountered in subtract
  cp1 -= tmp
C:\Users\jrunnels\AppData\Local\Continuum\anaconda3\lib\site-packages\numpy\core\numeric.py:1882: RuntimeWarning: overflow encountered in multiply
  multiply(a0, b1, out=cp2)
C:\Users\jrunnels\AppData\Local\Continuum\anaconda3\lib\site-packages\numpy\core\numeric.py:1883: RuntimeWarning: overflow encountered in multiply
  multiply(a1, b0, out=tmp)
C:\Users\jrunnels\AppData\Local\Continuum\anaconda3\lib\site-packages\numpy\core\numeric.py:1884: RuntimeWarning: invalid value encountered in subtract
  cp2 -= tmp
```

#### Scipy/Numpy/Python version information:
1.3.1 1.16.5 sys.version_info(major=3, minor=7, micro=4, releaselevel='final', serial=0)
<!-- 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)
```
-->


Discussion:

Resolving patch:
diff --git a/scipy/spatial/transform/rotation.py b/scipy/spatial/transform/rotation.py
--- a/scipy/spatial/transform/rotation.py
+++ b/scipy/spatial/transform/rotation.py
@@ -1413,7 +1413,7 @@ def __mul__(self, other):
         result = _compose_quat(self._quat, other._quat)
         if self._single and other._single:
             result = result[0]
-        return self.__class__(result, normalize=False, copy=False)
+        return self.__class__(result, normalize=True, copy=False)
 
     def inv(self):
         """Invert this rotation.


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
@@ -1124,3 +1124,11 @@ def test_slerp_call_scalar_time():
     delta = r_interpolated * r_interpolated_expected.inv()
 
     assert_allclose(delta.magnitude(), 0, atol=1e-16)
+
+
+def test_multiplication_stability():
+    qs = Rotation.random(50, random_state=0)
+    rs = Rotation.random(1000, random_state=1)
+    for q in qs:
+        rs *= q * rs
+        assert_allclose(np.linalg.norm(rs.as_quat(), axis=1), 1)