Fix spatial.transform.Rotation.from_euler/from_davenport shape consistency

As part of the effort to make Rotation broadcastable to arbitrary batch dimensions, we came across a shape inconsistency in the results of the Rotation.from_euler / from_davenport functions.

@j-bowhay proposed to take the discussion here to see if there are any objections to making a breaking change. Please let us know if you have a strong opinion on this one / are opposed to making this change without a major release.

The proposed changes are as follows:

Euler Angle Cases and Proposed Behavior

Case Sequence Input Shape Output Shape Behavior
0D "x" () (4,) Unchanged
0D "xyz" () raise Unchanged
1D "x" (1,) (1, 4)(4,) Changed
1D "xyz" (3,) (4,) Unchanged
2D "x" (N, 1) (N, 4) Unchanged
2D "xyz" (N, 3) (N, 4) Unchanged

Note: After the 2D case, standard broadcasting rules are followed already.

The logic here is that we promote 0D arrays, floats and ints to 1D. angles.shape[-1] must then be the same as num_axes, i.e. the last dimension of angles describes by how much we turn around each axis. Consequently, 0D arrays remain invalid for sequences with more than one axis. The resulting rotation has the shape np.atleast_1d(angles).shape[:-1].

Davenport Cases and Proposed Behavior

Case Axes Shape Angles Shape Output Shape Behavior
0D (3,) () (4,) Unchanged
1D (3,) (1,) (1, 4)(4,) Changed
1D (3,) (N,) (N, 4)raise Changed, raise if N != 1
1D (2, 3) (2,) (4,) Unchanged
2D (3,) (N, 1) (N, 4) Unchanged
2D (2, 3) (2, 1) (2, 4) Changed, raise 2 != 1
2D (2, 3) (2, 3) — (error) Unchanged
2D (1, 3) (2, 3) (2, 4) Changed, raise 1 != 3

Note: After the 2D case, standard broadcasting rules are followed already.

The logic here is that axes are promoted to at least 2D and angles to at least 1D. axes.shape[-2] must then match angles.shape[-1], i.e. as in euler, the last dimension of angles describes by how much we turn around each axis. Consequently, 0D arrays remain invalid for sequences of more than one axis. The resulting rotation has the shape np.broadcast_shapes(np.atleast_2d(axes).shape[:-2], np.atleast_1d(angles).shape[:-1]).

This is consistent with from_euler if we treat seq as a 2D array of axes and replace each letter with its respective turn axis.

Github issue for reference: MAINT: spatial.transform.Rotation: `from_euler/from_davenport` shape consistency · Issue #23568 · scipy/scipy · GitHub
Larger overview over what’s happening: ENH: spatial.transform: array API standard support tracker · Issue #23391 · scipy/scipy · GitHub