_rotation.py 105 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865
  1. from __future__ import annotations
  2. from collections.abc import Iterable, Iterator
  3. from types import EllipsisType, ModuleType, NotImplementedType
  4. import numpy as np
  5. import scipy.spatial.transform._rotation_cy as cython_backend
  6. import scipy.spatial.transform._rotation_xp as xp_backend
  7. from scipy.spatial.transform._rotation_groups import create_group
  8. from scipy._lib._array_api import (
  9. array_namespace,
  10. Array,
  11. is_numpy,
  12. ArrayLike,
  13. is_lazy_array,
  14. xp_capabilities,
  15. xp_promote,
  16. )
  17. from scipy._lib.array_api_compat import device as xp_device
  18. import scipy._lib.array_api_extra as xpx
  19. from scipy._lib._util import _transition_to_rng, broadcastable
  20. backend_registry = {array_namespace(np.empty(0)): cython_backend}
  21. def select_backend(xp: ModuleType, cython_compatible: bool):
  22. """Select the backend for the given array library.
  23. We need this selection function because the Cython backend for numpy does not
  24. support quaternions of arbitrary dimensions. We therefore only use the Array API
  25. backend for numpy if we are dealing with rotations of more than one leading
  26. dimension.
  27. """
  28. if is_numpy(xp) and not cython_compatible:
  29. return xp_backend
  30. return backend_registry.get(xp, xp_backend)
  31. @xp_capabilities()
  32. def _promote(*args: tuple[ArrayLike, ...], xp: ModuleType) -> Array:
  33. """Promote arrays to float64 for numpy, else according to the Array API spec.
  34. The return array dtype follows the following rules:
  35. - If quat is an ArrayLike or NumPy array, we always promote to float64
  36. - If quat is an Array from frameworks other than NumPy, we preserve the precision
  37. of the input array dtype.
  38. The first rule is required by the cython backend signatures that expect
  39. cython.double views. The second rule is necessary to promote non-floating arrays
  40. to the correct type in frameworks that may not support double precision (e.g.
  41. jax by default).
  42. """
  43. if is_numpy(xp):
  44. args += (np.empty(0, dtype=np.float64),) # Force float64 conversion
  45. out = xp_promote(*args, force_floating=True, xp=xp)
  46. if len(args) == 2: # One argument was passed + the added empty array
  47. return out[0]
  48. return out[:-1]
  49. return xp_promote(*args, force_floating=True, xp=xp)
  50. class Rotation:
  51. """Rotation in 3 dimensions.
  52. This class provides an interface to initialize from and represent rotations
  53. with:
  54. - Quaternions
  55. - Rotation Matrices
  56. - Rotation Vectors
  57. - Modified Rodrigues Parameters
  58. - Euler Angles
  59. - Davenport Angles (Generalized Euler Angles)
  60. The following operations on rotations are supported:
  61. - Application on vectors
  62. - Rotation Composition
  63. - Rotation Inversion
  64. - Rotation Indexing
  65. A `Rotation` instance can contain a single rotation transform or rotations of
  66. multiple leading dimensions. E.g., it is possible to have an N-dimensional array of
  67. (N, M, K) rotations. When applied to other rotations or vectors, standard
  68. broadcasting rules apply.
  69. Indexing within a rotation is supported to access a subset of the rotations stored
  70. in a `Rotation` instance.
  71. To create `Rotation` objects use ``from_...`` methods (see examples below).
  72. ``Rotation(...)`` is not supposed to be instantiated directly.
  73. Attributes
  74. ----------
  75. single
  76. Methods
  77. -------
  78. __len__
  79. from_quat
  80. from_matrix
  81. from_rotvec
  82. from_mrp
  83. from_euler
  84. from_davenport
  85. as_quat
  86. as_matrix
  87. as_rotvec
  88. as_mrp
  89. as_euler
  90. as_davenport
  91. concatenate
  92. apply
  93. __mul__
  94. __pow__
  95. inv
  96. magnitude
  97. approx_equal
  98. mean
  99. reduce
  100. create_group
  101. __getitem__
  102. identity
  103. random
  104. align_vectors
  105. See Also
  106. --------
  107. Slerp
  108. Notes
  109. -----
  110. .. versionadded:: 1.2.0
  111. Examples
  112. --------
  113. >>> from scipy.spatial.transform import Rotation as R
  114. >>> import numpy as np
  115. A `Rotation` instance can be initialized in any of the above formats and
  116. converted to any of the others. The underlying object is independent of the
  117. representation used for initialization.
  118. Consider a counter-clockwise rotation of 90 degrees about the z-axis. This
  119. corresponds to the following quaternion (in scalar-last format):
  120. >>> r = R.from_quat([0, 0, np.sin(np.pi/4), np.cos(np.pi/4)])
  121. The rotation can be expressed in any of the other formats:
  122. >>> r.as_matrix()
  123. array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00],
  124. [ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00],
  125. [ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
  126. >>> r.as_rotvec()
  127. array([0. , 0. , 1.57079633])
  128. >>> r.as_euler('zyx', degrees=True)
  129. array([90., 0., 0.])
  130. The same rotation can be initialized using a rotation matrix:
  131. >>> r = R.from_matrix([[0, -1, 0],
  132. ... [1, 0, 0],
  133. ... [0, 0, 1]])
  134. Representation in other formats:
  135. >>> r.as_quat()
  136. array([0. , 0. , 0.70710678, 0.70710678])
  137. >>> r.as_rotvec()
  138. array([0. , 0. , 1.57079633])
  139. >>> r.as_euler('zyx', degrees=True)
  140. array([90., 0., 0.])
  141. The rotation vector corresponding to this rotation is given by:
  142. >>> r = R.from_rotvec(np.pi/2 * np.array([0, 0, 1]))
  143. Representation in other formats:
  144. >>> r.as_quat()
  145. array([0. , 0. , 0.70710678, 0.70710678])
  146. >>> r.as_matrix()
  147. array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00],
  148. [ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00],
  149. [ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
  150. >>> r.as_euler('zyx', degrees=True)
  151. array([90., 0., 0.])
  152. The ``from_euler`` method is quite flexible in the range of input formats
  153. it supports. Here we initialize a single rotation about a single axis:
  154. >>> r = R.from_euler('z', 90, degrees=True)
  155. Again, the object is representation independent and can be converted to any
  156. other format:
  157. >>> r.as_quat()
  158. array([0. , 0. , 0.70710678, 0.70710678])
  159. >>> r.as_matrix()
  160. array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00],
  161. [ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00],
  162. [ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
  163. >>> r.as_rotvec()
  164. array([0. , 0. , 1.57079633])
  165. It is also possible to initialize multiple rotations in a single instance
  166. using any of the ``from_...`` functions. Here we initialize a stack of 3
  167. rotations using the ``from_euler`` method:
  168. >>> r = R.from_euler('zyx', [
  169. ... [90, 0, 0],
  170. ... [0, 45, 0],
  171. ... [45, 60, 30]], degrees=True)
  172. The other representations also now return a stack of 3 rotations. For
  173. example:
  174. >>> r.as_quat()
  175. array([[0. , 0. , 0.70710678, 0.70710678],
  176. [0. , 0.38268343, 0. , 0.92387953],
  177. [0.39190384, 0.36042341, 0.43967974, 0.72331741]])
  178. Applying the above rotations onto a vector:
  179. >>> v = [1, 2, 3]
  180. >>> r.apply(v)
  181. array([[-2. , 1. , 3. ],
  182. [ 2.82842712, 2. , 1.41421356],
  183. [ 2.24452282, 0.78093109, 2.89002836]])
  184. A `Rotation` instance can be indexed and sliced as if it were an ND array:
  185. >>> r.as_quat()
  186. array([[0. , 0. , 0.70710678, 0.70710678],
  187. [0. , 0.38268343, 0. , 0.92387953],
  188. [0.39190384, 0.36042341, 0.43967974, 0.72331741]])
  189. >>> p = r[0]
  190. >>> p.as_matrix()
  191. array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00],
  192. [ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00],
  193. [ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
  194. >>> q = r[1:3]
  195. >>> q.as_quat()
  196. array([[0. , 0.38268343, 0. , 0.92387953],
  197. [0.39190384, 0.36042341, 0.43967974, 0.72331741]])
  198. In fact it can be converted to numpy.array:
  199. >>> r_array = np.asarray(r)
  200. >>> r_array.shape
  201. (3,)
  202. >>> r_array[0].as_matrix()
  203. array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00],
  204. [ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00],
  205. [ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
  206. Multiple rotations can be composed using the ``*`` operator:
  207. >>> r1 = R.from_euler('z', 90, degrees=True)
  208. >>> r2 = R.from_rotvec([np.pi/4, 0, 0])
  209. >>> v = [1, 2, 3]
  210. >>> r2.apply(r1.apply(v))
  211. array([-2. , -1.41421356, 2.82842712])
  212. >>> r3 = r2 * r1 # Note the order
  213. >>> r3.apply(v)
  214. array([-2. , -1.41421356, 2.82842712])
  215. A rotation can be composed with itself using the ``**`` operator:
  216. >>> p = R.from_rotvec([1, 0, 0])
  217. >>> q = p ** 2
  218. >>> q.as_rotvec()
  219. array([2., 0., 0.])
  220. Finally, it is also possible to invert rotations:
  221. >>> r1 = R.from_euler('z', [[90], [45]], degrees=True)
  222. >>> r2 = r1.inv()
  223. >>> r2.as_euler('zyx', degrees=True)
  224. array([[-90., 0., 0.],
  225. [-45., 0., 0.]])
  226. The following function can be used to plot rotations with Matplotlib by
  227. showing how they transform the standard x, y, z coordinate axes:
  228. >>> import matplotlib.pyplot as plt
  229. >>> def plot_rotated_axes(ax, r, name=None, offset=(0, 0, 0), scale=1):
  230. ... colors = ("#FF6666", "#005533", "#1199EE") # Colorblind-safe RGB
  231. ... loc = np.array([offset, offset])
  232. ... for i, (axis, c) in enumerate(zip((ax.xaxis, ax.yaxis, ax.zaxis),
  233. ... colors)):
  234. ... axlabel = axis.axis_name
  235. ... axis.set_label_text(axlabel)
  236. ... axis.label.set_color(c)
  237. ... axis.line.set_color(c)
  238. ... axis.set_tick_params(colors=c)
  239. ... line = np.zeros((2, 3))
  240. ... line[1, i] = scale
  241. ... line_rot = r.apply(line)
  242. ... line_plot = line_rot + loc
  243. ... ax.plot(line_plot[:, 0], line_plot[:, 1], line_plot[:, 2], c)
  244. ... text_loc = line[1]*1.2
  245. ... text_loc_rot = r.apply(text_loc)
  246. ... text_plot = text_loc_rot + loc[0]
  247. ... ax.text(*text_plot, axlabel.upper(), color=c,
  248. ... va="center", ha="center")
  249. ... ax.text(*offset, name, color="k", va="center", ha="center",
  250. ... bbox={"fc": "w", "alpha": 0.8, "boxstyle": "circle"})
  251. Create three rotations - the identity and two Euler rotations using
  252. intrinsic and extrinsic conventions:
  253. >>> r0 = R.identity()
  254. >>> r1 = R.from_euler("ZYX", [90, -30, 0], degrees=True) # intrinsic
  255. >>> r2 = R.from_euler("zyx", [90, -30, 0], degrees=True) # extrinsic
  256. Add all three rotations to a single plot:
  257. >>> ax = plt.figure().add_subplot(projection="3d", proj_type="ortho")
  258. >>> plot_rotated_axes(ax, r0, name="r0", offset=(0, 0, 0))
  259. >>> plot_rotated_axes(ax, r1, name="r1", offset=(3, 0, 0))
  260. >>> plot_rotated_axes(ax, r2, name="r2", offset=(6, 0, 0))
  261. >>> _ = ax.annotate(
  262. ... "r0: Identity Rotation\\n"
  263. ... "r1: Intrinsic Euler Rotation (ZYX)\\n"
  264. ... "r2: Extrinsic Euler Rotation (zyx)",
  265. ... xy=(0.6, 0.7), xycoords="axes fraction", ha="left"
  266. ... )
  267. >>> ax.set(xlim=(-1.25, 7.25), ylim=(-1.25, 1.25), zlim=(-1.25, 1.25))
  268. >>> ax.set(xticks=range(-1, 8), yticks=[-1, 0, 1], zticks=[-1, 0, 1])
  269. >>> ax.set_aspect("equal", adjustable="box")
  270. >>> ax.figure.set_size_inches(6, 5)
  271. >>> plt.tight_layout()
  272. Show the plot:
  273. >>> plt.show()
  274. These examples serve as an overview into the `Rotation` class and highlight
  275. major functionalities. For more thorough examples of the range of input and
  276. output formats supported, consult the individual method's examples.
  277. """
  278. def __init__(
  279. self,
  280. quat: ArrayLike,
  281. normalize: bool = True,
  282. copy: bool = True,
  283. scalar_first: bool = False,
  284. ):
  285. xp = array_namespace(quat)
  286. self._xp = xp
  287. quat = _promote(quat, xp=xp)
  288. if quat.shape[-1] != 4:
  289. raise ValueError(
  290. f"Expected `quat` to have shape (..., 4), got {quat.shape}."
  291. )
  292. # Single NumPy quats or list of quats are accelerated by the cython backend.
  293. # This backend needs inputs with fixed ndim, so we always expand to 2D and
  294. # select the 0th element if quat was single to get the correct shape. For other
  295. # frameworks and quaternion tensors we use the generic array API backend.
  296. self._single = quat.ndim == 1 and is_numpy(xp)
  297. if self._single:
  298. quat = xpx.atleast_nd(quat, ndim=2, xp=xp)
  299. self._backend = select_backend(xp, cython_compatible=quat.ndim < 3)
  300. self._quat: Array = self._backend.from_quat(
  301. quat, normalize=normalize, copy=copy, scalar_first=scalar_first
  302. )
  303. @staticmethod
  304. @xp_capabilities(
  305. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  306. )
  307. def from_quat(quat: ArrayLike, *, scalar_first: bool = False) -> Rotation:
  308. """Initialize from quaternions.
  309. Rotations in 3 dimensions can be represented using unit norm
  310. quaternions [1]_.
  311. The 4 components of a quaternion are divided into a scalar part ``w``
  312. and a vector part ``(x, y, z)`` and can be expressed from the angle
  313. ``theta`` and the axis ``n`` of a rotation as follows::
  314. w = cos(theta / 2)
  315. x = sin(theta / 2) * n_x
  316. y = sin(theta / 2) * n_y
  317. z = sin(theta / 2) * n_z
  318. There are 2 conventions to order the components in a quaternion:
  319. - scalar-first order -- ``(w, x, y, z)``
  320. - scalar-last order -- ``(x, y, z, w)``
  321. The choice is controlled by `scalar_first` argument.
  322. By default, it is False and the scalar-last order is assumed.
  323. Advanced users may be interested in the "double cover" of 3D space by
  324. the quaternion representation [2]_. As of version 1.11.0, the
  325. following subset (and only this subset) of operations on a `Rotation`
  326. ``r`` corresponding to a quaternion ``q`` are guaranteed to preserve
  327. the double cover property: ``r = Rotation.from_quat(q)``,
  328. ``r.as_quat(canonical=False)``, ``r.inv()``, and composition using the
  329. ``*`` operator such as ``r*r``.
  330. Parameters
  331. ----------
  332. quat : array_like, shape (..., 4)
  333. Each row is a (possibly non-unit norm) quaternion representing an
  334. active rotation. Each quaternion will be normalized to unit norm.
  335. scalar_first : bool, optional
  336. Whether the scalar component goes first or last.
  337. Default is False, i.e. the scalar-last order is assumed.
  338. Returns
  339. -------
  340. rotation : `Rotation` instance
  341. Object containing the rotations represented by input quaternions.
  342. References
  343. ----------
  344. .. [1] https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
  345. .. [2] Hanson, Andrew J. "Visualizing quaternions."
  346. Morgan Kaufmann Publishers Inc., San Francisco, CA. 2006.
  347. Examples
  348. --------
  349. >>> from scipy.spatial.transform import Rotation as R
  350. A rotation can be initialized from a quaternion with the scalar-last
  351. (default) or scalar-first component order as shown below:
  352. >>> r = R.from_quat([0, 0, 0, 1])
  353. >>> r.as_matrix()
  354. array([[1., 0., 0.],
  355. [0., 1., 0.],
  356. [0., 0., 1.]])
  357. >>> r = R.from_quat([1, 0, 0, 0], scalar_first=True)
  358. >>> r.as_matrix()
  359. array([[1., 0., 0.],
  360. [0., 1., 0.],
  361. [0., 0., 1.]])
  362. It is possible to initialize multiple rotations in a single object by
  363. passing an N-dimensional array:
  364. >>> r = R.from_quat([[
  365. ... [1, 0, 0, 0],
  366. ... [0, 0, 0, 1]
  367. ... ]])
  368. >>> r.as_quat()
  369. array([[[1., 0., 0., 0.],
  370. [0., 0., 0., 1.]]])
  371. >>> r.as_quat().shape
  372. (1, 2, 4)
  373. It is also possible to have a stack of a single rotation:
  374. >>> r = R.from_quat([[0, 0, 0, 1]])
  375. >>> r.as_quat()
  376. array([[0., 0., 0., 1.]])
  377. >>> r.as_quat().shape
  378. (1, 4)
  379. Quaternions are normalized before initialization.
  380. >>> r = R.from_quat([0, 0, 1, 1])
  381. >>> r.as_quat()
  382. array([0. , 0. , 0.70710678, 0.70710678])
  383. """
  384. return Rotation(quat, normalize=True, scalar_first=scalar_first)
  385. @staticmethod
  386. @xp_capabilities(
  387. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  388. )
  389. def from_matrix(matrix: ArrayLike, *, assume_valid: bool = False) -> Rotation:
  390. """Initialize from rotation matrix.
  391. Rotations in 3 dimensions can be represented with 3 x 3 orthogonal
  392. matrices [1]_. If the input is not orthogonal, an approximation is
  393. created by orthogonalizing the input matrix using the method described
  394. in [2]_, and then converting the orthogonal rotation matrices to
  395. quaternions using the algorithm described in [3]_. Matrices must be
  396. right-handed.
  397. Parameters
  398. ----------
  399. matrix : array_like, shape (..., 3, 3)
  400. A single matrix or an ND array of matrices, where the last two dimensions
  401. contain the rotation matrices.
  402. assume_valid : bool, optional
  403. Must be False unless users can guarantee the input is a valid rotation
  404. matrix, i.e. it is orthogonal, rows and columns have unit norm and the
  405. determinant is 1. Setting this to True without ensuring these properties
  406. is unsafe and will silently lead to incorrect results. If True,
  407. normalization steps are skipped, which can improve runtime performance.
  408. Default is False.
  409. Returns
  410. -------
  411. rotation : `Rotation` instance
  412. Object containing the rotations represented by the rotation
  413. matrices.
  414. References
  415. ----------
  416. .. [1] https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions
  417. .. [2] https://en.wikipedia.org/wiki/Orthogonal_Procrustes_problem
  418. .. [3] F. Landis Markley, "Unit Quaternion from Rotation Matrix",
  419. Journal of guidance, control, and dynamics vol. 31.2, pp.
  420. 440-442, 2008.
  421. Examples
  422. --------
  423. >>> from scipy.spatial.transform import Rotation as R
  424. >>> import numpy as np
  425. Initialize a single rotation:
  426. >>> r = R.from_matrix([
  427. ... [0, -1, 0],
  428. ... [1, 0, 0],
  429. ... [0, 0, 1]])
  430. >>> r.single
  431. True
  432. >>> r.as_matrix().shape
  433. (3, 3)
  434. Initialize multiple rotations in a single object:
  435. >>> r = R.from_matrix([
  436. ... [
  437. ... [0, -1, 0],
  438. ... [1, 0, 0],
  439. ... [0, 0, 1],
  440. ... ],
  441. ... [
  442. ... [1, 0, 0],
  443. ... [0, 0, -1],
  444. ... [0, 1, 0],
  445. ... ]])
  446. >>> r.as_matrix().shape
  447. (2, 3, 3)
  448. >>> r.single
  449. False
  450. >>> len(r)
  451. 2
  452. If input matrices are not special orthogonal (orthogonal with
  453. determinant equal to +1), then a special orthogonal estimate is stored:
  454. >>> a = np.array([
  455. ... [0, -0.5, 0],
  456. ... [0.5, 0, 0],
  457. ... [0, 0, 0.5]])
  458. >>> np.linalg.det(a)
  459. 0.125
  460. >>> r = R.from_matrix(a)
  461. >>> matrix = r.as_matrix()
  462. >>> matrix
  463. array([[ 0., -1., 0.],
  464. [ 1., 0., 0.],
  465. [ 0., 0., 1.]])
  466. >>> np.linalg.det(matrix)
  467. 1.0
  468. It is also possible to have a stack containing a single rotation:
  469. >>> r = R.from_matrix([[
  470. ... [0, -1, 0],
  471. ... [1, 0, 0],
  472. ... [0, 0, 1]]])
  473. >>> r.as_matrix()
  474. array([[[ 0., -1., 0.],
  475. [ 1., 0., 0.],
  476. [ 0., 0., 1.]]])
  477. >>> r.as_matrix().shape
  478. (1, 3, 3)
  479. We can also create an N-dimensional array of rotations:
  480. >>> r = R.from_matrix(np.tile(np.eye(3), (2, 3, 1, 1)))
  481. >>> r.shape
  482. (2, 3)
  483. Notes
  484. -----
  485. This function was called from_dcm before.
  486. .. versionadded:: 1.4.0
  487. """
  488. xp = array_namespace(matrix)
  489. matrix = _promote(matrix, xp=xp)
  490. # Resulting quat will have 1 less dimension than matrix
  491. backend = select_backend(xp, cython_compatible=matrix.ndim < 4)
  492. quat = backend.from_matrix(matrix, assume_valid=assume_valid)
  493. return Rotation._from_raw_quat(quat, xp=xp, backend=backend)
  494. @staticmethod
  495. @xp_capabilities(
  496. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  497. )
  498. def from_rotvec(rotvec: ArrayLike, degrees: bool = False) -> Rotation:
  499. """Initialize from rotation vectors.
  500. A rotation vector is a 3 dimensional vector which is co-directional to
  501. the axis of rotation and whose norm gives the angle of rotation [1]_.
  502. Parameters
  503. ----------
  504. rotvec : array_like, shape (..., 3)
  505. A single vector or an ND array of vectors, where the last dimension
  506. contains the rotation vectors.
  507. degrees : bool, optional
  508. If True, then the given magnitudes are assumed to be in degrees.
  509. Default is False.
  510. .. versionadded:: 1.7.0
  511. Returns
  512. -------
  513. rotation : `Rotation` instance
  514. Object containing the rotations represented by input rotation
  515. vectors.
  516. References
  517. ----------
  518. .. [1] https://en.wikipedia.org/wiki/Axis%E2%80%93angle_representation#Rotation_vector
  519. Examples
  520. --------
  521. >>> from scipy.spatial.transform import Rotation as R
  522. >>> import numpy as np
  523. Initialize a single rotation:
  524. >>> r = R.from_rotvec(np.pi/2 * np.array([0, 0, 1]))
  525. >>> r.as_rotvec()
  526. array([0. , 0. , 1.57079633])
  527. >>> r.as_rotvec().shape
  528. (3,)
  529. Initialize a rotation in degrees, and view it in degrees:
  530. >>> r = R.from_rotvec(45 * np.array([0, 1, 0]), degrees=True)
  531. >>> r.as_rotvec(degrees=True)
  532. array([ 0., 45., 0.])
  533. Initialize multiple rotations in one object:
  534. >>> r = R.from_rotvec([
  535. ... [0, 0, np.pi/2],
  536. ... [np.pi/2, 0, 0]])
  537. >>> r.as_rotvec()
  538. array([[0. , 0. , 1.57079633],
  539. [1.57079633, 0. , 0. ]])
  540. >>> r.as_rotvec().shape
  541. (2, 3)
  542. It is also possible to have a stack of a single rotation:
  543. >>> r = R.from_rotvec([[0, 0, np.pi/2]])
  544. >>> r.as_rotvec().shape
  545. (1, 3)
  546. """
  547. xp = array_namespace(rotvec)
  548. rotvec = _promote(rotvec, xp=xp)
  549. backend = select_backend(xp, cython_compatible=rotvec.ndim < 3)
  550. quat = backend.from_rotvec(rotvec, degrees=degrees)
  551. return Rotation._from_raw_quat(quat, xp=xp, backend=backend)
  552. @staticmethod
  553. @xp_capabilities(
  554. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  555. )
  556. def from_euler(seq: str, angles: ArrayLike, degrees: bool = False) -> Rotation:
  557. """Initialize from Euler angles.
  558. Rotations in 3-D can be represented by a sequence of 3
  559. rotations around a sequence of axes. In theory, any three axes spanning
  560. the 3-D Euclidean space are enough. In practice, the axes of rotation are
  561. chosen to be the basis vectors.
  562. The three rotations can either be in a global frame of reference
  563. (extrinsic) or in a body centred frame of reference (intrinsic), which
  564. is attached to, and moves with, the object under rotation [1]_.
  565. Parameters
  566. ----------
  567. seq : string
  568. Specifies sequence of axes for rotations. Up to 3 characters
  569. belonging to the set {'X', 'Y', 'Z'} for intrinsic rotations, or
  570. {'x', 'y', 'z'} for extrinsic rotations. Extrinsic and intrinsic
  571. rotations cannot be mixed in one function call.
  572. angles : float or array_like, shape (..., [1 or 2 or 3])
  573. Euler angles specified in radians (`degrees` is False) or degrees
  574. (`degrees` is True).
  575. Each character in `seq` defines one axis around which `angles` turns.
  576. The resulting rotation has the shape np.atleast_1d(angles).shape[:-1].
  577. Dimensionless angles are thus only valid for single character `seq`.
  578. degrees : bool, optional
  579. If True, then the given angles are assumed to be in degrees.
  580. Default is False.
  581. Returns
  582. -------
  583. rotation : `Rotation` instance
  584. Object containing the rotation represented by the sequence of
  585. rotations around given axes with given angles.
  586. References
  587. ----------
  588. .. [1] https://en.wikipedia.org/wiki/Euler_angles#Definition_by_intrinsic_rotations
  589. Examples
  590. --------
  591. >>> from scipy.spatial.transform import Rotation as R
  592. Initialize a single rotation along a single axis:
  593. >>> r = R.from_euler('x', 90, degrees=True)
  594. >>> r.as_quat().shape
  595. (4,)
  596. Initialize a single rotation with a given axis sequence:
  597. >>> r = R.from_euler('zyx', [90, 45, 30], degrees=True)
  598. >>> r.as_quat().shape
  599. (4,)
  600. Initialize a stack with a single rotation around a single axis:
  601. >>> r = R.from_euler('x', [[90]], degrees=True)
  602. >>> r.as_quat().shape
  603. (1, 4)
  604. Initialize a stack with a single rotation with an axis sequence:
  605. >>> r = R.from_euler('zyx', [[90, 45, 30]], degrees=True)
  606. >>> r.as_quat().shape
  607. (1, 4)
  608. Initialize multiple elementary rotations in one object:
  609. >>> r = R.from_euler('x', [[90], [45], [30]], degrees=True)
  610. >>> r.as_quat().shape
  611. (3, 4)
  612. Initialize multiple rotations in one object:
  613. >>> r = R.from_euler('zyx', [[90, 45, 30], [35, 45, 90]], degrees=True)
  614. >>> r.as_quat().shape
  615. (2, 4)
  616. """
  617. xp = array_namespace(angles)
  618. angles = _promote(angles, xp=xp)
  619. backend = select_backend(xp, cython_compatible=angles.ndim < 3)
  620. quat = backend.from_euler(seq, angles, degrees=degrees)
  621. return Rotation._from_raw_quat(quat, xp=xp, backend=backend)
  622. @staticmethod
  623. @xp_capabilities(
  624. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  625. )
  626. def from_davenport(
  627. axes: ArrayLike,
  628. order: str,
  629. angles: ArrayLike | float,
  630. degrees: bool = False,
  631. ) -> Rotation:
  632. """Initialize from Davenport angles.
  633. Rotations in 3-D can be represented by a sequence of 3
  634. rotations around a sequence of axes.
  635. The three rotations can either be in a global frame of reference
  636. (extrinsic) or in a body centred frame of reference (intrinsic), which
  637. is attached to, and moves with, the object under rotation [1]_.
  638. For both Euler angles and Davenport angles, consecutive axes must
  639. be are orthogonal (``axis2`` is orthogonal to both ``axis1`` and
  640. ``axis3``). For Euler angles, there is an additional relationship
  641. between ``axis1`` or ``axis3``, with two possibilities:
  642. - ``axis1`` and ``axis3`` are also orthogonal (asymmetric sequence)
  643. - ``axis1 == axis3`` (symmetric sequence)
  644. For Davenport angles, this last relationship is relaxed [2]_, and only
  645. the consecutive orthogonal axes requirement is maintained.
  646. Parameters
  647. ----------
  648. axes : array_like, shape (3,) or (..., [1 or 2 or 3], 3)
  649. Axis of rotation, if one dimensional. If two or more dimensional, describes
  650. the sequence of axes for rotations, where each axes[..., i, :] is the ith
  651. axis. If more than one axis is given, then the second axis must be
  652. orthogonal to both the first and third axes.
  653. order : string
  654. If it is equal to 'e' or 'extrinsic', the sequence will be
  655. extrinsic. If it is equal to 'i' or 'intrinsic', sequence
  656. will be treated as intrinsic.
  657. angles : float or array_like, shape (..., [1 or 2 or 3])
  658. Angles specified in radians (`degrees` is False) or degrees
  659. (`degrees` is True).
  660. Each angle i in the last dimension of `angles` turns around the corresponding
  661. axis axis[..., i, :]. The resulting rotation has the shape
  662. np.broadcast_shapes(np.atleast_2d(axes).shape[:-2], np.atleast_1d(angles).shape[:-1])
  663. Dimensionless angles are thus only valid for a single axis.
  664. degrees : bool, optional
  665. If True, then the given angles are assumed to be in degrees.
  666. Default is False.
  667. Returns
  668. -------
  669. rotation : `Rotation` instance
  670. Object containing the rotation represented by the sequence of
  671. rotations around given axes with given angles.
  672. References
  673. ----------
  674. .. [1] https://en.wikipedia.org/wiki/Euler_angles#Definition_by_intrinsic_rotations
  675. .. [2] Shuster, Malcolm & Markley, Landis. (2003). Generalization of
  676. the Euler Angles. Journal of the Astronautical Sciences. 51. 123-132.
  677. 10.1007/BF03546304.
  678. Examples
  679. --------
  680. >>> from scipy.spatial.transform import Rotation as R
  681. Davenport angles are a generalization of Euler angles, when we use the
  682. canonical basis axes:
  683. >>> ex = [1, 0, 0]
  684. >>> ey = [0, 1, 0]
  685. >>> ez = [0, 0, 1]
  686. Initialize a single rotation with a given axis sequence:
  687. >>> axes = [ez, ey, ex]
  688. >>> r = R.from_davenport(axes, 'extrinsic', [90, 0, 0], degrees=True)
  689. >>> r.as_quat().shape
  690. (4,)
  691. It is equivalent to Euler angles in this case:
  692. >>> r.as_euler('zyx', degrees=True)
  693. array([90., 0., -0.])
  694. Initialize multiple rotations in one object:
  695. >>> r = R.from_davenport(axes, 'extrinsic', [[90, 45, 30], [35, 45, 90]], degrees=True)
  696. >>> r.as_quat().shape
  697. (2, 4)
  698. Using only one or two axes is also possible:
  699. >>> r = R.from_davenport([ez, ex], 'extrinsic', [[90, 45], [35, 45]], degrees=True)
  700. >>> r.as_quat().shape
  701. (2, 4)
  702. Non-canonical axes are possible, and they do not need to be normalized,
  703. as long as consecutive axes are orthogonal:
  704. >>> e1 = [2, 0, 0]
  705. >>> e2 = [0, 1, 0]
  706. >>> e3 = [1, 0, 1]
  707. >>> axes = [e1, e2, e3]
  708. >>> r = R.from_davenport(axes, 'extrinsic', [90, 45, 30], degrees=True)
  709. >>> r.as_quat()
  710. [ 0.701057, 0.430459, -0.092296, 0.560986]
  711. """ # noqa: E501
  712. xp = array_namespace(axes)
  713. axes, angles = _promote(axes, angles, xp=xp)
  714. cython_compatible = axes.ndim < 3 and angles.ndim < 2
  715. backend = select_backend(xp, cython_compatible=cython_compatible)
  716. quat = backend.from_davenport(axes, order, angles, degrees)
  717. return Rotation._from_raw_quat(quat, xp=xp, backend=backend)
  718. @staticmethod
  719. @xp_capabilities(
  720. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  721. )
  722. def from_mrp(mrp: ArrayLike) -> Rotation:
  723. """Initialize from Modified Rodrigues Parameters (MRPs).
  724. MRPs are a 3 dimensional vector co-directional to the axis of rotation and whose
  725. magnitude is equal to ``tan(theta / 4)``, where ``theta`` is the angle of
  726. rotation (in radians) [1]_.
  727. MRPs have a singularity at 360 degrees which can be avoided by ensuring the
  728. angle of rotation does not exceed 180 degrees, i.e. switching the direction of
  729. the rotation when it is past 180 degrees.
  730. Parameters
  731. ----------
  732. mrp : array_like, shape (..., 3)
  733. A single vector or an ND array of vectors, where the last dimension
  734. contains the rotation parameters.
  735. Returns
  736. -------
  737. rotation : `Rotation` instance
  738. Object containing the rotations represented by input MRPs.
  739. References
  740. ----------
  741. .. [1] Shuster, M. D. "A Survey of Attitude Representations",
  742. The Journal of Astronautical Sciences, Vol. 41, No.4, 1993,
  743. pp. 475-476
  744. Notes
  745. -----
  746. .. versionadded:: 1.6.0
  747. Examples
  748. --------
  749. >>> from scipy.spatial.transform import Rotation as R
  750. >>> import numpy as np
  751. Initialize a single rotation:
  752. >>> r = R.from_mrp([0, 0, 1])
  753. >>> r.as_euler('xyz', degrees=True)
  754. array([0. , 0. , 180. ])
  755. >>> r.as_euler('xyz').shape
  756. (3,)
  757. Initialize multiple rotations in one object:
  758. >>> r = R.from_mrp([
  759. ... [0, 0, 1],
  760. ... [1, 0, 0]])
  761. >>> r.as_euler('xyz', degrees=True)
  762. array([[0. , 0. , 180. ],
  763. [180.0 , 0. , 0. ]])
  764. >>> r.as_euler('xyz').shape
  765. (2, 3)
  766. It is also possible to have a stack of a single rotation:
  767. >>> r = R.from_mrp([[0, 0, np.pi/2]])
  768. >>> r.as_euler('xyz').shape
  769. (1, 3)
  770. """
  771. xp = array_namespace(mrp)
  772. mrp = _promote(mrp, xp=xp)
  773. backend = select_backend(xp, cython_compatible=mrp.ndim < 3)
  774. quat = backend.from_mrp(mrp)
  775. return Rotation._from_raw_quat(quat, xp=xp, backend=backend)
  776. @xp_capabilities(
  777. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  778. )
  779. def as_quat(self, canonical: bool = False, *, scalar_first: bool = False) -> Array:
  780. """Represent as quaternions.
  781. Rotations in 3 dimensions can be represented using unit norm
  782. quaternions [1]_.
  783. The 4 components of a quaternion are divided into a scalar part ``w``
  784. and a vector part ``(x, y, z)`` and can be expressed from the angle
  785. ``theta`` and the axis ``n`` of a rotation as follows::
  786. w = cos(theta / 2)
  787. x = sin(theta / 2) * n_x
  788. y = sin(theta / 2) * n_y
  789. z = sin(theta / 2) * n_z
  790. There are 2 conventions to order the components in a quaternion:
  791. - scalar-first order -- ``(w, x, y, z)``
  792. - scalar-last order -- ``(x, y, z, w)``
  793. The choice is controlled by `scalar_first` argument.
  794. By default, it is False and the scalar-last order is used.
  795. The mapping from quaternions to rotations is
  796. two-to-one, i.e. quaternions ``q`` and ``-q``, where ``-q`` simply
  797. reverses the sign of each component, represent the same spatial
  798. rotation.
  799. Parameters
  800. ----------
  801. canonical : `bool`, default False
  802. Whether to map the redundant double cover of rotation space to a
  803. unique "canonical" single cover. If True, then the quaternion is
  804. chosen from {q, -q} such that the w term is positive. If the w term
  805. is 0, then the quaternion is chosen such that the first nonzero
  806. term of the x, y, and z terms is positive.
  807. scalar_first : bool, optional
  808. Whether the scalar component goes first or last.
  809. Default is False, i.e. the scalar-last order is used.
  810. Returns
  811. -------
  812. quat : `numpy.ndarray`, shape (..., 4)
  813. Shape depends on shape of inputs used for initialization.
  814. References
  815. ----------
  816. .. [1] https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
  817. Examples
  818. --------
  819. >>> from scipy.spatial.transform import Rotation as R
  820. >>> import numpy as np
  821. A rotation can be represented as a quaternion with either scalar-last
  822. (default) or scalar-first component order.
  823. This is shown for a single rotation:
  824. >>> r = R.from_matrix(np.eye(3))
  825. >>> r.as_quat()
  826. array([0., 0., 0., 1.])
  827. >>> r.as_quat(scalar_first=True)
  828. array([1., 0., 0., 0.])
  829. The resulting shape of the quaternion is always the shape of the Rotation
  830. object with an added last dimension of size 4. E.g. when the `Rotation` object
  831. contains an N-dimensional array (N, M, K) of rotations, the result will be a
  832. 4-dimensional array:
  833. >>> r = R.from_rotvec(np.ones((2, 3, 4, 3)))
  834. >>> r.as_quat().shape
  835. (2, 3, 4, 4)
  836. Quaternions can be mapped from a redundant double cover of the
  837. rotation space to a canonical representation with a positive w term.
  838. >>> r = R.from_quat([0, 0, 0, -1])
  839. >>> r.as_quat()
  840. array([0. , 0. , 0. , -1.])
  841. >>> r.as_quat(canonical=True)
  842. array([0. , 0. , 0. , 1.])
  843. """
  844. quat = self._backend.as_quat(
  845. self._quat, canonical=canonical, scalar_first=scalar_first
  846. )
  847. if self._single:
  848. return quat[0, ...]
  849. return quat
  850. @xp_capabilities(
  851. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  852. )
  853. def as_matrix(self) -> Array:
  854. """Represent as rotation matrix.
  855. 3D rotations can be represented using rotation matrices, which
  856. are 3 x 3 real orthogonal matrices with determinant equal to +1 [1]_.
  857. Returns
  858. -------
  859. matrix : ndarray, shape (..., 3)
  860. Shape depends on shape of inputs used for initialization.
  861. References
  862. ----------
  863. .. [1] https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions
  864. Examples
  865. --------
  866. >>> from scipy.spatial.transform import Rotation as R
  867. >>> import numpy as np
  868. Represent a single rotation:
  869. >>> r = R.from_rotvec([0, 0, np.pi/2])
  870. >>> r.as_matrix()
  871. array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00],
  872. [ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00],
  873. [ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
  874. >>> r.as_matrix().shape
  875. (3, 3)
  876. Represent a stack with a single rotation:
  877. >>> r = R.from_quat([[1, 1, 0, 0]])
  878. >>> r.as_matrix()
  879. array([[[ 0., 1., 0.],
  880. [ 1., 0., 0.],
  881. [ 0., 0., -1.]]])
  882. >>> r.as_matrix().shape
  883. (1, 3, 3)
  884. Represent multiple rotations:
  885. >>> r = R.from_rotvec([[np.pi/2, 0, 0], [0, 0, np.pi/2]])
  886. >>> r.as_matrix()
  887. array([[[ 1.00000000e+00, 0.00000000e+00, 0.00000000e+00],
  888. [ 0.00000000e+00, 2.22044605e-16, -1.00000000e+00],
  889. [ 0.00000000e+00, 1.00000000e+00, 2.22044605e-16]],
  890. [[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00],
  891. [ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00],
  892. [ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]])
  893. >>> r.as_matrix().shape
  894. (2, 3, 3)
  895. Notes
  896. -----
  897. This function was called as_dcm before.
  898. .. versionadded:: 1.4.0
  899. """
  900. matrix = self._backend.as_matrix(self._quat)
  901. if self._single:
  902. return matrix[0, ...]
  903. return matrix
  904. @xp_capabilities(
  905. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  906. )
  907. def as_rotvec(self, degrees: bool = False) -> Array:
  908. """Represent as rotation vectors.
  909. A rotation vector is a 3 dimensional vector which is co-directional to
  910. the axis of rotation and whose norm gives the angle of rotation [1]_.
  911. Parameters
  912. ----------
  913. degrees : boolean, optional
  914. Returned magnitudes are in degrees if this flag is True, else they are
  915. in radians. Default is False.
  916. .. versionadded:: 1.7.0
  917. Returns
  918. -------
  919. rotvec : ndarray, shape (..., 3)
  920. Shape depends on shape of inputs used for initialization.
  921. References
  922. ----------
  923. .. [1] https://en.wikipedia.org/wiki/Axis%E2%80%93angle_representation#Rotation_vector
  924. Examples
  925. --------
  926. >>> from scipy.spatial.transform import Rotation as R
  927. >>> import numpy as np
  928. Represent a single rotation:
  929. >>> r = R.from_euler('z', 90, degrees=True)
  930. >>> r.as_rotvec()
  931. array([0. , 0. , 1.57079633])
  932. >>> r.as_rotvec().shape
  933. (3,)
  934. Represent a rotation in degrees:
  935. >>> r = R.from_euler('YX', (-90, -90), degrees=True)
  936. >>> s = r.as_rotvec(degrees=True)
  937. >>> s
  938. array([-69.2820323, -69.2820323, -69.2820323])
  939. >>> np.linalg.norm(s)
  940. 120.00000000000001
  941. Represent a stack with a single rotation:
  942. >>> r = R.from_quat([[0, 0, 1, 1]])
  943. >>> r.as_rotvec()
  944. array([[0. , 0. , 1.57079633]])
  945. >>> r.as_rotvec().shape
  946. (1, 3)
  947. Represent multiple rotations in a single object:
  948. >>> r = R.from_quat([[0, 0, 1, 1], [1, 1, 0, 1]])
  949. >>> r.as_rotvec()
  950. array([[0. , 0. , 1.57079633],
  951. [1.35102172, 1.35102172, 0. ]])
  952. >>> r.as_rotvec().shape
  953. (2, 3)
  954. """
  955. rotvec = self._backend.as_rotvec(self._quat, degrees=degrees)
  956. if self._single:
  957. return rotvec[0, ...]
  958. return rotvec
  959. @xp_capabilities(
  960. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  961. )
  962. def as_euler(
  963. self, seq: str, degrees: bool = False, *, suppress_warnings: bool = False
  964. ) -> Array:
  965. """Represent as Euler angles.
  966. Any orientation can be expressed as a composition of 3 elementary
  967. rotations. Once the axis sequence has been chosen, Euler angles define
  968. the angle of rotation around each respective axis [1]_.
  969. The algorithm from [2]_ has been used to calculate Euler angles for the
  970. rotation about a given sequence of axes.
  971. Euler angles suffer from the problem of gimbal lock [3]_, where the
  972. representation loses a degree of freedom and it is not possible to
  973. determine the first and third angles uniquely. In this case,
  974. a warning is raised (unless the ``suppress_warnings`` option is used),
  975. and the third angle is set to zero. Note however that the returned
  976. angles still represent the correct rotation.
  977. Parameters
  978. ----------
  979. seq : string, length 3
  980. 3 characters belonging to the set {'X', 'Y', 'Z'} for intrinsic
  981. rotations, or {'x', 'y', 'z'} for extrinsic rotations [1]_.
  982. Adjacent axes cannot be the same.
  983. Extrinsic and intrinsic rotations cannot be mixed in one function
  984. call.
  985. degrees : boolean, optional
  986. Returned angles are in degrees if this flag is True, else they are
  987. in radians. Default is False.
  988. suppress_warnings : boolean, optional
  989. Disable warnings about gimbal lock. Default is False.
  990. Returns
  991. -------
  992. angles : ndarray, shape (..., 3)
  993. Shape depends on shape of inputs used to initialize object.
  994. The returned angles are in the range:
  995. - First angle belongs to [-180, 180] degrees (both inclusive)
  996. - Third angle belongs to [-180, 180] degrees (both inclusive)
  997. - Second angle belongs to:
  998. - [-90, 90] degrees if all axes are different (like xyz)
  999. - [0, 180] degrees if first and third axes are the same
  1000. (like zxz)
  1001. References
  1002. ----------
  1003. .. [1] https://en.wikipedia.org/wiki/Euler_angles#Definition_by_intrinsic_rotations
  1004. .. [2] Bernardes E, Viollet S (2022) Quaternion to Euler angles
  1005. conversion: A direct, general and computationally efficient
  1006. method. PLoS ONE 17(11): e0276302.
  1007. https://doi.org/10.1371/journal.pone.0276302
  1008. .. [3] https://en.wikipedia.org/wiki/Gimbal_lock#In_applied_mathematics
  1009. Examples
  1010. --------
  1011. >>> from scipy.spatial.transform import Rotation as R
  1012. >>> import numpy as np
  1013. Represent a single rotation:
  1014. >>> r = R.from_rotvec([0, 0, np.pi/2])
  1015. >>> r.as_euler('zxy', degrees=True)
  1016. array([90., 0., 0.])
  1017. >>> r.as_euler('zxy', degrees=True).shape
  1018. (3,)
  1019. Represent a stack of single rotation:
  1020. >>> r = R.from_rotvec([[0, 0, np.pi/2]])
  1021. >>> r.as_euler('zxy', degrees=True)
  1022. array([[90., 0., 0.]])
  1023. >>> r.as_euler('zxy', degrees=True).shape
  1024. (1, 3)
  1025. Represent multiple rotations in a single object:
  1026. >>> r = R.from_rotvec([
  1027. ... [0, 0, np.pi/2],
  1028. ... [0, -np.pi/3, 0],
  1029. ... [np.pi/4, 0, 0]])
  1030. >>> r.as_euler('zxy', degrees=True)
  1031. array([[ 90., 0., 0.],
  1032. [ 0., 0., -60.],
  1033. [ 0., 45., 0.]])
  1034. >>> r.as_euler('zxy', degrees=True).shape
  1035. (3, 3)
  1036. """
  1037. euler = self._backend.as_euler(
  1038. self._quat, seq, degrees=degrees, suppress_warnings=suppress_warnings
  1039. )
  1040. if self._single:
  1041. return euler[0, ...]
  1042. return euler
  1043. @xp_capabilities(
  1044. skip_backends=[
  1045. ("dask.array", "missing linalg.cross/det functions and .mT attribute"),
  1046. ("cupy", "missing .mT attribute in cupy<14.*"),
  1047. ]
  1048. )
  1049. def as_davenport(
  1050. self,
  1051. axes: ArrayLike,
  1052. order: str,
  1053. degrees: bool = False,
  1054. *,
  1055. suppress_warnings: bool = False,
  1056. ) -> Array:
  1057. """Represent as Davenport angles.
  1058. Any orientation can be expressed as a composition of 3 elementary
  1059. rotations.
  1060. For both Euler angles and Davenport angles, consecutive axes must
  1061. be are orthogonal (``axis2`` is orthogonal to both ``axis1`` and
  1062. ``axis3``). For Euler angles, there is an additional relationship
  1063. between ``axis1`` or ``axis3``, with two possibilities:
  1064. - ``axis1`` and ``axis3`` are also orthogonal (asymmetric sequence)
  1065. - ``axis1 == axis3`` (symmetric sequence)
  1066. For Davenport angles, this last relationship is relaxed [1]_, and only
  1067. the consecutive orthogonal axes requirement is maintained.
  1068. A slightly modified version of the algorithm from [2]_ has been used to
  1069. calculate Davenport angles for the rotation about a given sequence of
  1070. axes.
  1071. Davenport angles, just like Euler angles, suffer from the problem of
  1072. gimbal lock [3]_, where the representation loses a degree of freedom
  1073. and it is not possible to determine the first and third angles
  1074. uniquely. In this case, a warning is raised (unless the
  1075. ``suppress_warnings`` option is used), and the third angle is set
  1076. to zero. Note however that the returned angles still represent the
  1077. correct rotation.
  1078. Parameters
  1079. ----------
  1080. axes : array_like, shape (..., [1 or 2 or 3], 3) or (..., 3)
  1081. Axis of rotation, if one dimensional. If N dimensional, describes the
  1082. sequence of axes for rotations, where each axes[..., i, :] is the ith
  1083. axis. If more than one axis is given, then the second axis must be
  1084. orthogonal to both the first and third axes.
  1085. order : string
  1086. If it belongs to the set {'e', 'extrinsic'}, the sequence will be
  1087. extrinsic. If it belongs to the set {'i', 'intrinsic'}, sequence
  1088. will be treated as intrinsic.
  1089. degrees : boolean, optional
  1090. Returned angles are in degrees if this flag is True, else they are
  1091. in radians. Default is False.
  1092. suppress_warnings : boolean, optional
  1093. Disable warnings about gimbal lock. Default is False.
  1094. Returns
  1095. -------
  1096. angles : ndarray, shape (..., 3)
  1097. Shape depends on shape of inputs used to initialize object.
  1098. The returned angles are in the range:
  1099. - First angle belongs to [-180, 180] degrees (both inclusive)
  1100. - Third angle belongs to [-180, 180] degrees (both inclusive)
  1101. - Second angle belongs to a set of size 180 degrees,
  1102. given by: ``[-abs(lambda), 180 - abs(lambda)]``, where ``lambda``
  1103. is the angle between the first and third axes.
  1104. References
  1105. ----------
  1106. .. [1] Shuster, Malcolm & Markley, Landis. (2003). Generalization of
  1107. the Euler Angles. Journal of the Astronautical Sciences. 51. 123-132.
  1108. 10.1007/BF03546304.
  1109. .. [2] Bernardes E, Viollet S (2022) Quaternion to Euler angles
  1110. conversion: A direct, general and computationally efficient method.
  1111. PLoS ONE 17(11): e0276302. 10.1371/journal.pone.0276302
  1112. .. [3] https://en.wikipedia.org/wiki/Gimbal_lock#In_applied_mathematics
  1113. Examples
  1114. --------
  1115. >>> from scipy.spatial.transform import Rotation as R
  1116. >>> import numpy as np
  1117. Davenport angles are a generalization of Euler angles, when we use the
  1118. canonical basis axes:
  1119. >>> ex = [1, 0, 0]
  1120. >>> ey = [0, 1, 0]
  1121. >>> ez = [0, 0, 1]
  1122. Represent a single rotation:
  1123. >>> r = R.from_rotvec([0, 0, np.pi/2])
  1124. >>> r.as_davenport([ez, ex, ey], 'extrinsic', degrees=True)
  1125. array([90., 0., 0.])
  1126. >>> r.as_euler('zxy', degrees=True)
  1127. array([90., 0., 0.])
  1128. >>> r.as_davenport([ez, ex, ey], 'extrinsic', degrees=True).shape
  1129. (3,)
  1130. Represent a stack of single rotation:
  1131. >>> r = R.from_rotvec([[0, 0, np.pi/2]])
  1132. >>> r.as_davenport([ez, ex, ey], 'extrinsic', degrees=True)
  1133. array([[90., 0., 0.]])
  1134. >>> r.as_davenport([ez, ex, ey], 'extrinsic', degrees=True).shape
  1135. (1, 3)
  1136. Represent multiple rotations in a single object:
  1137. >>> r = R.from_rotvec([
  1138. ... [0, 0, 90],
  1139. ... [45, 0, 0]], degrees=True)
  1140. >>> r.as_davenport([ez, ex, ey], 'extrinsic', degrees=True)
  1141. array([[90., 0., 0.],
  1142. [ 0., 45., 0.]])
  1143. >>> r.as_davenport([ez, ex, ey], 'extrinsic', degrees=True).shape
  1144. (2, 3)
  1145. """
  1146. axes = self._xp.asarray(
  1147. axes, dtype=self._quat.dtype, device=xp_device(self._quat)
  1148. )
  1149. davenport = self._backend.as_davenport(
  1150. self._quat, axes, order, degrees, suppress_warnings=suppress_warnings
  1151. )
  1152. if self._single:
  1153. return davenport[0, ...]
  1154. return davenport
  1155. @xp_capabilities(
  1156. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  1157. )
  1158. def as_mrp(self) -> Array:
  1159. """Represent as Modified Rodrigues Parameters (MRPs).
  1160. MRPs are a 3 dimensional vector co-directional to the axis of rotation and whose
  1161. magnitude is equal to ``tan(theta / 4)``, where ``theta`` is the angle of
  1162. rotation (in radians) [1]_.
  1163. MRPs have a singularity at 360 degrees which can be avoided by ensuring the
  1164. angle of rotation does not exceed 180 degrees, i.e. switching the direction of
  1165. the rotation when it is past 180 degrees. This function will always return MRPs
  1166. corresponding to a rotation of less than or equal to 180 degrees.
  1167. Returns
  1168. -------
  1169. mrps : ndarray, shape (..., 3)
  1170. Shape depends on shape of inputs used for initialization.
  1171. References
  1172. ----------
  1173. .. [1] Shuster, M. D. "A Survey of Attitude Representations",
  1174. The Journal of Astronautical Sciences, Vol. 41, No.4, 1993,
  1175. pp. 475-476
  1176. Examples
  1177. --------
  1178. >>> from scipy.spatial.transform import Rotation as R
  1179. >>> import numpy as np
  1180. Represent a single rotation:
  1181. >>> r = R.from_rotvec([0, 0, np.pi])
  1182. >>> r.as_mrp()
  1183. array([0. , 0. , 1. ])
  1184. >>> r.as_mrp().shape
  1185. (3,)
  1186. Represent a stack with a single rotation:
  1187. >>> r = R.from_euler('xyz', [[180, 0, 0]], degrees=True)
  1188. >>> r.as_mrp()
  1189. array([[1. , 0. , 0. ]])
  1190. >>> r.as_mrp().shape
  1191. (1, 3)
  1192. Represent multiple rotations:
  1193. >>> r = R.from_rotvec([[np.pi/2, 0, 0], [0, 0, np.pi/2]])
  1194. >>> r.as_mrp()
  1195. array([[0.41421356, 0. , 0. ],
  1196. [0. , 0. , 0.41421356]])
  1197. >>> r.as_mrp().shape
  1198. (2, 3)
  1199. Notes
  1200. -----
  1201. .. versionadded:: 1.6.0
  1202. """
  1203. mrp = self._backend.as_mrp(self._quat)
  1204. if self._single:
  1205. return mrp[0, ...]
  1206. return mrp
  1207. @staticmethod
  1208. @xp_capabilities(
  1209. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  1210. )
  1211. def concatenate(rotations: Rotation | Iterable[Rotation]) -> Rotation:
  1212. """Concatenate a sequence of `Rotation` objects into a single object.
  1213. This is useful if you want to, for example, take the mean of a set of
  1214. rotations and need to pack them into a single object to do so.
  1215. Parameters
  1216. ----------
  1217. rotations : sequence of `Rotation` objects
  1218. The rotations to concatenate. If a single `Rotation` object is
  1219. passed in, a copy is returned.
  1220. Returns
  1221. -------
  1222. concatenated : `Rotation` instance
  1223. The concatenated rotations.
  1224. Examples
  1225. --------
  1226. >>> from scipy.spatial.transform import Rotation as R
  1227. >>> r1 = R.from_rotvec([0, 0, 1])
  1228. >>> r2 = R.from_rotvec([0, 0, 2])
  1229. >>> rc = R.concatenate([r1, r2])
  1230. >>> rc.as_rotvec()
  1231. array([[0., 0., 1.],
  1232. [0., 0., 2.]])
  1233. >>> rc.mean().as_rotvec()
  1234. array([0., 0., 1.5])
  1235. Concatenation of a split rotation recovers the original object.
  1236. >>> rs = [r for r in rc]
  1237. >>> R.concatenate(rs).as_rotvec()
  1238. array([[0., 0., 1.],
  1239. [0., 0., 2.]])
  1240. Note that it may be simpler to create the desired rotations by passing
  1241. in a single list of the data during initialization, rather then by
  1242. concatenating:
  1243. >>> R.from_rotvec([[0, 0, 1], [0, 0, 2]]).as_rotvec()
  1244. array([[0., 0., 1.],
  1245. [0., 0., 2.]])
  1246. Notes
  1247. -----
  1248. .. versionadded:: 1.8.0
  1249. """
  1250. if isinstance(rotations, Rotation):
  1251. return Rotation(rotations.as_quat(), normalize=False, copy=True)
  1252. if not all(isinstance(x, Rotation) for x in rotations):
  1253. raise TypeError("input must contain Rotation objects only")
  1254. xp = array_namespace(rotations[0].as_quat())
  1255. quats = xp.concat(
  1256. [xpx.atleast_nd(x.as_quat(), ndim=2, xp=xp) for x in rotations]
  1257. )
  1258. return Rotation._from_raw_quat(quats, xp=xp)
  1259. @xp_capabilities(
  1260. skip_backends=[
  1261. ("dask.array", "missing linalg.cross/det functions and .mT attribute"),
  1262. ("cupy", "missing .mT attribute in cupy<14.*"),
  1263. ]
  1264. )
  1265. def apply(self, vectors: ArrayLike, inverse: bool = False) -> Array:
  1266. """Apply this rotation to a set of vectors.
  1267. If the original frame rotates to the final frame by this rotation, then
  1268. its application to a vector can be seen in two ways:
  1269. - As a projection of vector components expressed in the final frame
  1270. to the original frame.
  1271. - As the physical rotation of a vector being glued to the original
  1272. frame as it rotates. In this case the vector components are
  1273. expressed in the original frame before and after the rotation.
  1274. In terms of rotation matrices, this application is the same as
  1275. ``self.as_matrix() @ vectors``.
  1276. Parameters
  1277. ----------
  1278. vectors : array_like, shape (..., 3)
  1279. Each `vectors[..., :]` represents a vector in 3D space. The shape of
  1280. rotations and shape of vectors given must follow standard numpy
  1281. broadcasting rules: either one of them equals unity or they both
  1282. equal each other.
  1283. inverse : boolean, optional
  1284. If True then the inverse of the rotation(s) is applied to the input
  1285. vectors. Default is False.
  1286. Returns
  1287. -------
  1288. rotated_vectors : ndarray, shape (..., 3)
  1289. Result of applying rotation on input vectors.
  1290. Shape is determined according to numpy broadcasting rules. I.e., the result
  1291. will have the shape `np.broadcast_shapes(r.shape, v.shape[:-1]) + (3,)`
  1292. Examples
  1293. --------
  1294. >>> from scipy.spatial.transform import Rotation as R
  1295. >>> import numpy as np
  1296. Single rotation applied on a single vector:
  1297. >>> vector = np.array([1, 0, 0])
  1298. >>> r = R.from_rotvec([0, 0, np.pi/2])
  1299. >>> r.as_matrix()
  1300. array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00],
  1301. [ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00],
  1302. [ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
  1303. >>> r.apply(vector)
  1304. array([2.22044605e-16, 1.00000000e+00, 0.00000000e+00])
  1305. >>> r.apply(vector).shape
  1306. (3,)
  1307. Single rotation applied on multiple vectors:
  1308. >>> vectors = np.array([
  1309. ... [1, 0, 0],
  1310. ... [1, 2, 3]])
  1311. >>> r = R.from_rotvec([0, 0, np.pi/4])
  1312. >>> r.as_matrix()
  1313. array([[ 0.70710678, -0.70710678, 0. ],
  1314. [ 0.70710678, 0.70710678, 0. ],
  1315. [ 0. , 0. , 1. ]])
  1316. >>> r.apply(vectors)
  1317. array([[ 0.70710678, 0.70710678, 0. ],
  1318. [-0.70710678, 2.12132034, 3. ]])
  1319. >>> r.apply(vectors).shape
  1320. (2, 3)
  1321. Multiple rotations on a single vector:
  1322. >>> r = R.from_rotvec([[0, 0, np.pi/4], [np.pi/2, 0, 0]])
  1323. >>> vector = np.array([1,2,3])
  1324. >>> r.as_matrix()
  1325. array([[[ 7.07106781e-01, -7.07106781e-01, 0.00000000e+00],
  1326. [ 7.07106781e-01, 7.07106781e-01, 0.00000000e+00],
  1327. [ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]],
  1328. [[ 1.00000000e+00, 0.00000000e+00, 0.00000000e+00],
  1329. [ 0.00000000e+00, 2.22044605e-16, -1.00000000e+00],
  1330. [ 0.00000000e+00, 1.00000000e+00, 2.22044605e-16]]])
  1331. >>> r.apply(vector)
  1332. array([[-0.70710678, 2.12132034, 3. ],
  1333. [ 1. , -3. , 2. ]])
  1334. >>> r.apply(vector).shape
  1335. (2, 3)
  1336. Multiple rotations on multiple vectors. Each rotation is applied on the
  1337. corresponding vector:
  1338. >>> r = R.from_euler('zxy', [
  1339. ... [0, 0, 90],
  1340. ... [45, 30, 60]], degrees=True)
  1341. >>> vectors = [
  1342. ... [1, 2, 3],
  1343. ... [1, 0, -1]]
  1344. >>> r.apply(vectors)
  1345. array([[ 3. , 2. , -1. ],
  1346. [-0.09026039, 1.11237244, -0.86860844]])
  1347. >>> r.apply(vectors).shape
  1348. (2, 3)
  1349. Broadcasting rules apply:
  1350. >>> r = R.from_rotvec(np.tile([0, 0, np.pi/4], (5, 1, 4, 1)))
  1351. >>> vectors = np.ones((3, 4, 3))
  1352. >>> r.shape, vectors.shape
  1353. ((5, 1, 4), (3, 4, 3))
  1354. >>> r.apply(vectors).shape
  1355. (5, 3, 4, 3)
  1356. It is also possible to apply the inverse rotation:
  1357. >>> r = R.from_euler('zxy', [
  1358. ... [0, 0, 90],
  1359. ... [45, 30, 60]], degrees=True)
  1360. >>> vectors = [
  1361. ... [1, 2, 3],
  1362. ... [1, 0, -1]]
  1363. >>> r.apply(vectors, inverse=True)
  1364. array([[-3. , 2. , 1. ],
  1365. [ 1.09533535, -0.8365163 , 0.3169873 ]])
  1366. """
  1367. vectors = self._xp.asarray(
  1368. vectors, device=xp_device(self._quat), dtype=self._quat.dtype
  1369. )
  1370. single_vector = vectors.ndim == 1
  1371. # Numpy optimization: The Cython backend typing requires us to have fixed
  1372. # dimensions, so for the Numpy case we always broadcast the vector to 2D.
  1373. if vectors.shape[-1] != 3:
  1374. raise ValueError(f"Expected input of shape (..., 3), got {vectors.shape}.")
  1375. if is_numpy(self._xp):
  1376. vectors = xpx.atleast_nd(vectors, ndim=2, xp=self._xp)
  1377. cython_compatible = self._quat.ndim < 3 and vectors.ndim < 3
  1378. backend = select_backend(self._xp, cython_compatible=cython_compatible)
  1379. result = backend.apply(self._quat, vectors, inverse=inverse)
  1380. if self._single and single_vector:
  1381. return result[0, ...]
  1382. return result
  1383. @xp_capabilities(
  1384. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  1385. )
  1386. def __mul__(self, other: Rotation) -> Rotation | NotImplementedType:
  1387. """Compose this rotation with the other.
  1388. If `p` and `q` are two rotations, then the composition of 'q followed
  1389. by p' is equivalent to `p * q`. In terms of rotation matrices,
  1390. the composition can be expressed as
  1391. ``p.as_matrix() @ q.as_matrix()``.
  1392. Parameters
  1393. ----------
  1394. other : `Rotation` instance
  1395. Object containing the rotations to be composed with this one. Note
  1396. that rotation compositions are not commutative, so ``p * q`` is
  1397. generally different from ``q * p``.
  1398. Returns
  1399. -------
  1400. composition : `Rotation` instance
  1401. This function supports composition of multiple rotations at a time.
  1402. Composition follows standard numpy broadcasting rules. The resulting
  1403. `Rotation` object will have the shape
  1404. `np.broadcast_shapes(p.shape, q.shape)`. In dimensions with size > 1,
  1405. rotations are composed with matching indices. In dimensions with only
  1406. one rotation, the single rotation is composed with each rotation in the
  1407. other object.
  1408. Examples
  1409. --------
  1410. >>> from scipy.spatial.transform import Rotation as R
  1411. >>> import numpy as np
  1412. Composition of two single rotations:
  1413. >>> p = R.from_quat([0, 0, 1, 1])
  1414. >>> q = R.from_quat([1, 0, 0, 1])
  1415. >>> p.as_matrix()
  1416. array([[ 0., -1., 0.],
  1417. [ 1., 0., 0.],
  1418. [ 0., 0., 1.]])
  1419. >>> q.as_matrix()
  1420. array([[ 1., 0., 0.],
  1421. [ 0., 0., -1.],
  1422. [ 0., 1., 0.]])
  1423. >>> r = p * q
  1424. >>> r.as_matrix()
  1425. array([[0., 0., 1.],
  1426. [1., 0., 0.],
  1427. [0., 1., 0.]])
  1428. Composition of two objects containing equal number of rotations:
  1429. >>> p = R.from_quat([[0, 0, 1, 1], [1, 0, 0, 1]])
  1430. >>> q = R.from_rotvec([[np.pi/4, 0, 0], [-np.pi/4, 0, np.pi/4]])
  1431. >>> p.as_quat()
  1432. array([[0. , 0. , 0.70710678, 0.70710678],
  1433. [0.70710678, 0. , 0. , 0.70710678]])
  1434. >>> q.as_quat()
  1435. array([[ 0.38268343, 0. , 0. , 0.92387953],
  1436. [-0.37282173, 0. , 0.37282173, 0.84971049]])
  1437. >>> r = p * q
  1438. >>> r.as_quat()
  1439. array([[ 0.27059805, 0.27059805, 0.65328148, 0.65328148],
  1440. [ 0.33721128, -0.26362477, 0.26362477, 0.86446082]])
  1441. Broadcasting rules apply:
  1442. >>> p = R.from_quat(np.tile(np.array([0, 0, 1, 1]), (5, 1, 1)))
  1443. >>> q = R.from_quat(np.tile(np.array([1, 0, 0, 1]), (1, 6, 1)))
  1444. >>> p.shape, q.shape
  1445. ((5, 1), (1, 6))
  1446. >>> r = p * q
  1447. >>> r.shape
  1448. (5, 6)
  1449. """
  1450. # Check that other is a Rotation object. We want to return NotImplemented
  1451. # instead of raising an error to allow other types to implement __rmul__.
  1452. # Python will then automatically try to delegate the multiplication to the
  1453. # other type.
  1454. # See https://github.com/scipy/scipy/issues/21541
  1455. if not isinstance(other, Rotation):
  1456. return NotImplemented
  1457. if not broadcastable(self._quat.shape, other._quat.shape):
  1458. raise ValueError(
  1459. f"Cannot broadcast {self._quat.shape[:-1]} rotations in "
  1460. f"first to {other._quat.shape[:-1]} rotations in second object."
  1461. )
  1462. cython_compatible = self._quat.ndim < 3 and other._quat.ndim < 3
  1463. backend = select_backend(self._xp, cython_compatible=cython_compatible)
  1464. quat = backend.compose_quat(self._quat, other._quat)
  1465. if self._single and other._single:
  1466. quat = quat[0]
  1467. return Rotation(quat, normalize=True, copy=False)
  1468. @xp_capabilities(
  1469. skip_backends=[("dask.array", "cannot handle zero-length rotations")]
  1470. )
  1471. def __pow__(self, n: float | Array, modulus: None = None) -> Rotation:
  1472. """Compose this rotation with itself `n` times.
  1473. Composition of a rotation ``p`` with itself can be extended to
  1474. non-integer ``n`` by considering the power ``n`` to be a scale factor
  1475. applied to the angle of rotation about the rotation's fixed axis. The
  1476. expression ``q = p ** n`` can also be expressed as
  1477. ``q = Rotation.from_rotvec(n * p.as_rotvec())``.
  1478. If ``n`` is negative, then the rotation is inverted before the power
  1479. is applied. In other words, ``p ** -abs(n) == p.inv() ** abs(n)``.
  1480. Parameters
  1481. ----------
  1482. n : float | Array
  1483. The number of times to compose the rotation with itself. If `n` is
  1484. an array, then it must be 0d or 1d with shape (1,).
  1485. modulus : None
  1486. This overridden argument is not applicable to Rotations and must be
  1487. ``None``.
  1488. Returns
  1489. -------
  1490. power : `Rotation` instance
  1491. The resulting rotation will be of the same shape as the original rotation
  1492. object. Each element of the output is the corresponding element of the
  1493. input rotation raised to the power of ``n``.
  1494. Notes
  1495. -----
  1496. For example, a power of 2 will double the angle of rotation, and a
  1497. power of 0.5 will halve the angle. There are three notable cases: if
  1498. ``n == 1`` then the original rotation is returned, if ``n == 0``
  1499. then the identity rotation is returned, and if ``n == -1`` then
  1500. ``p.inv()`` is returned.
  1501. Note that fractional powers ``n`` which effectively take a root of
  1502. rotation, do so using the shortest path smallest representation of that
  1503. angle (the principal root). This means that powers of ``n`` and ``1/n``
  1504. are not necessarily inverses of each other. For example, a 0.5 power of
  1505. a +240 degree rotation will be calculated as the 0.5 power of a -120
  1506. degree rotation, with the result being a rotation of -60 rather than
  1507. +120 degrees.
  1508. Examples
  1509. --------
  1510. >>> from scipy.spatial.transform import Rotation as R
  1511. Raising a rotation to a power:
  1512. >>> p = R.from_rotvec([1, 0, 0])
  1513. >>> q = p ** 2
  1514. >>> q.as_rotvec()
  1515. array([2., 0., 0.])
  1516. >>> r = p ** 0.5
  1517. >>> r.as_rotvec()
  1518. array([0.5, 0., 0.])
  1519. Inverse powers do not necessarily cancel out:
  1520. >>> p = R.from_rotvec([0, 0, 120], degrees=True)
  1521. >>> ((p ** 2) ** 0.5).as_rotvec(degrees=True)
  1522. array([ -0., -0., -60.])
  1523. """
  1524. if modulus is not None:
  1525. raise NotImplementedError("modulus not supported")
  1526. quat = self._backend.pow(self._quat, n)
  1527. if self._single:
  1528. quat = quat[0]
  1529. return Rotation._from_raw_quat(quat, xp=self._xp, backend=self._backend)
  1530. @xp_capabilities(
  1531. skip_backends=[("dask.array", "cannot handle zero-length rotations")]
  1532. )
  1533. def inv(self) -> Rotation:
  1534. """Invert this rotation.
  1535. Composition of a rotation with its inverse results in an identity
  1536. transformation.
  1537. Returns
  1538. -------
  1539. inverse : `Rotation` instance
  1540. Object containing inverse of the rotations in the current instance.
  1541. Examples
  1542. --------
  1543. >>> from scipy.spatial.transform import Rotation as R
  1544. >>> import numpy as np
  1545. Inverting a single rotation:
  1546. >>> p = R.from_euler('z', 45, degrees=True)
  1547. >>> q = p.inv()
  1548. >>> q.as_euler('zyx', degrees=True)
  1549. array([-45., 0., 0.])
  1550. Inverting multiple rotations:
  1551. >>> p = R.from_rotvec([[0, 0, np.pi/3], [-np.pi/4, 0, 0]])
  1552. >>> q = p.inv()
  1553. >>> q.as_rotvec()
  1554. array([[-0. , -0. , -1.04719755],
  1555. [ 0.78539816, -0. , -0. ]])
  1556. """
  1557. q_inv = self._backend.inv(self._quat)
  1558. if self._single:
  1559. q_inv = q_inv[0, ...]
  1560. return Rotation._from_raw_quat(q_inv, xp=self._xp, backend=self._backend)
  1561. @xp_capabilities(
  1562. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  1563. )
  1564. def magnitude(self) -> Array:
  1565. """Get the magnitude(s) of the rotation(s).
  1566. Returns
  1567. -------
  1568. magnitude : ndarray or float
  1569. Angle(s) in radians, float if object contains a single rotation
  1570. and ndarray if object contains ND rotations. The magnitude
  1571. will always be in the range [0, pi].
  1572. Examples
  1573. --------
  1574. >>> from scipy.spatial.transform import Rotation as R
  1575. >>> import numpy as np
  1576. >>> r = R.from_quat(np.eye(4))
  1577. >>> r.as_quat()
  1578. array([[ 1., 0., 0., 0.],
  1579. [ 0., 1., 0., 0.],
  1580. [ 0., 0., 1., 0.],
  1581. [ 0., 0., 0., 1.]])
  1582. >>> r.magnitude()
  1583. array([3.14159265, 3.14159265, 3.14159265, 0. ])
  1584. Magnitude of a single rotation:
  1585. >>> r[0].magnitude()
  1586. 3.141592653589793
  1587. """
  1588. magnitude = self._backend.magnitude(self._quat)
  1589. if self._single:
  1590. # Special handling for numpy and single rotations. self._single is only set
  1591. # if xp is numpy. We therefore know that magnitude is a numpy array and
  1592. # that it contains a single element. Previously this code returned a Python
  1593. # float in that case. Here we return a numpy float64 scalar. All other
  1594. # Array API libraries return 0d arrays instead.
  1595. # See https://github.com/scipy/scipy/pull/23198#issuecomment-3003757848
  1596. return magnitude[0]
  1597. return magnitude
  1598. @xp_capabilities(
  1599. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  1600. )
  1601. def approx_equal(
  1602. self, other: Rotation, atol: float | None = None, degrees: bool = False
  1603. ) -> Array:
  1604. """Determine if another rotation is approximately equal to this one.
  1605. Equality is measured by calculating the smallest angle between the
  1606. rotations, and checking to see if it is smaller than `atol`.
  1607. Parameters
  1608. ----------
  1609. other : `Rotation` instance
  1610. Object containing the rotations to measure against this one.
  1611. atol : float, optional
  1612. The absolute angular tolerance, below which the rotations are
  1613. considered equal. If not given, then set to 1e-8 radians by
  1614. default.
  1615. degrees : bool, optional
  1616. If True and `atol` is given, then `atol` is measured in degrees. If
  1617. False (default), then atol is measured in radians.
  1618. Returns
  1619. -------
  1620. approx_equal : ndarray or bool
  1621. Whether the rotations are approximately equal, bool if object
  1622. contains a single rotation and ndarray if object contains multiple
  1623. rotations.
  1624. Examples
  1625. --------
  1626. >>> from scipy.spatial.transform import Rotation as R
  1627. >>> import numpy as np
  1628. >>> p = R.from_quat([0, 0, 0, 1])
  1629. >>> q = R.from_quat(np.eye(4))
  1630. >>> p.approx_equal(q)
  1631. array([False, False, False, True])
  1632. Approximate equality for a single rotation:
  1633. >>> p.approx_equal(q[0])
  1634. False
  1635. """
  1636. cython_compatible = self._quat.ndim < 3 and other._quat.ndim < 3
  1637. backend = select_backend(self._xp, cython_compatible=cython_compatible)
  1638. return backend.approx_equal(self._quat, other._quat, atol=atol, degrees=degrees)
  1639. @xp_capabilities(
  1640. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  1641. )
  1642. def mean(
  1643. self,
  1644. weights: ArrayLike | None = None,
  1645. axis: None | int | tuple[int, ...] = None,
  1646. ) -> Rotation:
  1647. """Get the mean of the rotations.
  1648. The mean used is the chordal L2 mean (also called the projected or
  1649. induced arithmetic mean) [1]_. If ``A`` is a set of rotation matrices,
  1650. then the mean ``M`` is the rotation matrix that minimizes the
  1651. following loss function:
  1652. .. math::
  1653. L(M) = \\sum_{i = 1}^{n} w_i \\lVert \\mathbf{A}_i -
  1654. \\mathbf{M} \\rVert^2 ,
  1655. where :math:`w_i`'s are the `weights` corresponding to each matrix.
  1656. Parameters
  1657. ----------
  1658. weights : array_like shape (..., N), optional
  1659. Weights describing the relative importance of the rotations. If
  1660. None (default), then all values in `weights` are assumed to be
  1661. equal. If given, the shape of `weights` must be broadcastable to
  1662. the rotation shape. Weights must be non-negative.
  1663. axis : None, int, or tuple of ints, optional
  1664. Axis or axes along which the means are computed. The default is to
  1665. compute the mean of all rotations.
  1666. Returns
  1667. -------
  1668. mean : `Rotation` instance
  1669. Single rotation containing the mean of the rotations in the
  1670. current instance.
  1671. References
  1672. ----------
  1673. .. [1] Hartley, Richard, et al.,
  1674. "Rotation Averaging", International Journal of Computer Vision
  1675. 103, 2013, pp. 267-305.
  1676. Examples
  1677. --------
  1678. >>> from scipy.spatial.transform import Rotation as R
  1679. >>> r = R.from_euler('zyx', [[0, 0, 0],
  1680. ... [1, 0, 0],
  1681. ... [0, 1, 0],
  1682. ... [0, 0, 1]], degrees=True)
  1683. >>> r.mean().as_euler('zyx', degrees=True)
  1684. array([0.24945696, 0.25054542, 0.24945696])
  1685. """
  1686. mean = self._backend.mean(self._quat, weights=weights, axis=axis)
  1687. return Rotation._from_raw_quat(mean, xp=self._xp, backend=self._backend)
  1688. @xp_capabilities(
  1689. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  1690. )
  1691. def reduce(
  1692. self,
  1693. left: Rotation | None = None,
  1694. right: Rotation | None = None,
  1695. return_indices: bool = False,
  1696. ) -> Rotation | tuple[Rotation, Array, Array]:
  1697. """Reduce this rotation with the provided rotation groups.
  1698. Reduction of a rotation ``p`` is a transformation of the form
  1699. ``q = l * p * r``, where ``l`` and ``r`` are chosen from `left` and
  1700. `right` respectively, such that rotation ``q`` has the smallest
  1701. magnitude.
  1702. If `left` and `right` are rotation groups representing symmetries of
  1703. two objects rotated by ``p``, then ``q`` is the rotation of the
  1704. smallest magnitude to align these objects considering their symmetries.
  1705. Parameters
  1706. ----------
  1707. left : `Rotation` instance, optional
  1708. Object containing the left rotation(s). Default value (None)
  1709. corresponds to the identity rotation.
  1710. right : `Rotation` instance, optional
  1711. Object containing the right rotation(s). Default value (None)
  1712. corresponds to the identity rotation.
  1713. return_indices : bool, optional
  1714. Whether to return the indices of the rotations from `left` and
  1715. `right` used for reduction.
  1716. Returns
  1717. -------
  1718. reduced : `Rotation` instance
  1719. Object containing reduced rotations.
  1720. left_best, right_best: integer ndarray
  1721. Indices of elements from `left` and `right` used for reduction.
  1722. """
  1723. left = left.as_quat() if left is not None else None
  1724. right = right.as_quat() if right is not None else None
  1725. reduced, left_idx, right_idx = self._backend.reduce(
  1726. self._quat, left=left, right=right
  1727. )
  1728. if self._single:
  1729. reduced = reduced[0, ...]
  1730. rot = Rotation._from_raw_quat(reduced, xp=self._xp, backend=self._backend)
  1731. if return_indices:
  1732. left_idx = left_idx if left is not None else None
  1733. right_idx = right_idx if right is not None else None
  1734. return rot, left_idx, right_idx
  1735. return rot
  1736. @classmethod
  1737. def create_group(cls, group: str, axis: str = "Z") -> Rotation:
  1738. """Create a 3D rotation group.
  1739. Parameters
  1740. ----------
  1741. group : string
  1742. The name of the group. Must be one of 'I', 'O', 'T', 'Dn', 'Cn',
  1743. where `n` is a positive integer. The groups are:
  1744. * I: Icosahedral group
  1745. * O: Octahedral group
  1746. * T: Tetrahedral group
  1747. * D: Dicyclic group
  1748. * C: Cyclic group
  1749. axis : integer
  1750. The cyclic rotation axis. Must be one of ['X', 'Y', 'Z'] (or
  1751. lowercase). Default is 'Z'. Ignored for groups 'I', 'O', and 'T'.
  1752. Returns
  1753. -------
  1754. rotation : `Rotation` instance
  1755. Object containing the elements of the rotation group.
  1756. Notes
  1757. -----
  1758. This method generates rotation groups only. The full 3-dimensional
  1759. point groups [PointGroups]_ also contain reflections.
  1760. References
  1761. ----------
  1762. .. [PointGroups] `Point groups
  1763. <https://en.wikipedia.org/wiki/Point_groups_in_three_dimensions>`_
  1764. on Wikipedia.
  1765. """
  1766. # TODO: We defer the implementation of groups for arbitrary Array API frameworks
  1767. # to the follow-up PR that adds general Array API support for Rotations.
  1768. return create_group(cls, group, axis=axis)
  1769. @xp_capabilities(
  1770. jax_jit=False,
  1771. skip_backends=[("dask.array", "cannot handle zero-length rotations")],
  1772. )
  1773. def __getitem__(self, indexer: int | slice | EllipsisType | None) -> Rotation:
  1774. """Extract rotation(s) at given index(es) from object.
  1775. Create a new `Rotation` instance containing a subset of rotations
  1776. stored in this object.
  1777. Parameters
  1778. ----------
  1779. indexer : index, slice, or index array
  1780. Specifies which rotation(s) to extract. A single indexer must be
  1781. specified, i.e. as if indexing a 1 dimensional array or list.
  1782. Returns
  1783. -------
  1784. rotation : `Rotation` instance
  1785. Contains
  1786. - a single rotation, if `indexer` is a single index
  1787. - a stack of rotation(s), if `indexer` is a slice, or and index
  1788. array.
  1789. Raises
  1790. ------
  1791. TypeError if the instance was created as a single rotation.
  1792. Examples
  1793. --------
  1794. >>> from scipy.spatial.transform import Rotation as R
  1795. >>> rs = R.from_quat([
  1796. ... [1, 1, 0, 0],
  1797. ... [0, 1, 0, 1],
  1798. ... [1, 1, -1, 0]]) # These quats are normalized
  1799. >>> rs.as_quat()
  1800. array([[ 0.70710678, 0.70710678, 0. , 0. ],
  1801. [ 0. , 0.70710678, 0. , 0.70710678],
  1802. [ 0.57735027, 0.57735027, -0.57735027, 0. ]])
  1803. Indexing using a single index:
  1804. >>> a = rs[0]
  1805. >>> a.as_quat()
  1806. array([0.70710678, 0.70710678, 0. , 0. ])
  1807. Array slicing:
  1808. >>> b = rs[1:3]
  1809. >>> b.as_quat()
  1810. array([[ 0. , 0.70710678, 0. , 0.70710678],
  1811. [ 0.57735027, 0.57735027, -0.57735027, 0. ]])
  1812. List comprehension to split each rotation into its own object:
  1813. >>> c = [r for r in rs]
  1814. >>> print([r.as_quat() for r in c])
  1815. [array([ 0.70710678, 0.70710678, 0. , 0. ]),
  1816. array([ 0. , 0.70710678, 0. , 0.70710678]),
  1817. array([ 0.57735027, 0.57735027, -0.57735027, 0. ])]
  1818. Concatenation of split rotations will recover the original object:
  1819. >>> R.concatenate([a, b]).as_quat()
  1820. array([[ 0.70710678, 0.70710678, 0. , 0. ],
  1821. [ 0. , 0.70710678, 0. , 0.70710678],
  1822. [ 0.57735027, 0.57735027, -0.57735027, 0. ]])
  1823. """
  1824. if self._single or self._quat.ndim == 1:
  1825. raise TypeError("Single rotation is not subscriptable.")
  1826. is_array = isinstance(indexer, type(self._quat))
  1827. # Masking is only specified in the Array API when the array is the sole index
  1828. # TODO: This special case handling is mainly a result of Array API limitations.
  1829. # Ideally we would get rid of them altogether and converge to [indexer, ...]
  1830. # indexing.
  1831. if is_array and indexer.dtype == self._xp.bool:
  1832. return Rotation(self._quat[indexer], normalize=False)
  1833. if is_array and self._xp.isdtype(indexer.dtype, "integral"):
  1834. # xp.take is implementation-defined for zero-dim arrays, hence we raise
  1835. # pre-emptively to have consistent behavior across frameworks.
  1836. if self._quat.shape[0] == 0:
  1837. raise IndexError("cannot do a non-empty take from an empty axes.")
  1838. return Rotation(self._xp.take(self._quat, indexer, axis=0), normalize=False)
  1839. return Rotation(self._quat[indexer, ...], normalize=False)
  1840. @xp_capabilities(
  1841. jax_jit=False,
  1842. skip_backends=[("dask.array", "cannot handle zero-length rotations")],
  1843. )
  1844. def __setitem__(self, indexer: int | slice | EllipsisType | None, value: Rotation):
  1845. """Set rotation(s) at given index(es) from object.
  1846. Parameters
  1847. ----------
  1848. indexer : index, slice, or index array
  1849. Specifies which rotation(s) to replace. A single indexer must be
  1850. specified, i.e. as if indexing a 1 dimensional array or list.
  1851. value : `Rotation` instance
  1852. The rotations to set.
  1853. Raises
  1854. ------
  1855. TypeError if the instance was created as a single rotation.
  1856. Notes
  1857. -----
  1858. .. versionadded:: 1.8.0
  1859. """
  1860. if self._single or self._quat.ndim == 1:
  1861. raise TypeError("Single rotation is not subscriptable.")
  1862. if not isinstance(value, Rotation):
  1863. raise TypeError("value must be a Rotation object")
  1864. self._quat = self._backend.setitem(self._quat, value.as_quat(), indexer)
  1865. @staticmethod
  1866. def identity(
  1867. num: int | None = None, *, shape: int | tuple[int, ...] | None = None
  1868. ) -> Rotation:
  1869. """Get identity rotation(s).
  1870. Composition with the identity rotation has no effect.
  1871. Parameters
  1872. ----------
  1873. num : int or None, optional
  1874. Number of identity rotations to generate. If None (default), then a
  1875. single rotation is generated.
  1876. shape : int or tuple of ints, optional
  1877. Shape of identity rotations to generate. If specified, `num` must
  1878. be None.
  1879. Returns
  1880. -------
  1881. identity : Rotation object
  1882. The identity rotation.
  1883. """
  1884. # TODO: We should move to one single way of specifying the output shape and
  1885. # deprecate `num`.
  1886. if num is not None and shape is not None:
  1887. raise ValueError("Only one of `num` or `shape` can be specified.")
  1888. quat = cython_backend.identity(num, shape=shape)
  1889. return Rotation._from_raw_quat(quat, xp=array_namespace(quat))
  1890. @staticmethod
  1891. @_transition_to_rng("random_state", position_num=2)
  1892. def random(
  1893. num: int | None = None,
  1894. rng: np.random.Generator | None = None,
  1895. *,
  1896. shape: tuple[int, ...] | None = None,
  1897. ) -> Rotation:
  1898. r"""Generate rotations that are uniformly distributed on a sphere.
  1899. Formally, the rotations follow the Haar-uniform distribution over the SO(3)
  1900. group.
  1901. Parameters
  1902. ----------
  1903. num : int or None, optional
  1904. Number of random rotations to generate. If None (default), then a
  1905. single rotation is generated.
  1906. rng : `numpy.random.Generator`, optional
  1907. Pseudorandom number generator state. When `rng` is None, a new
  1908. `numpy.random.Generator` is created using entropy from the
  1909. operating system. Types other than `numpy.random.Generator` are
  1910. passed to `numpy.random.default_rng` to instantiate a `Generator`.
  1911. shape : tuple of ints, optional
  1912. Shape of random rotations to generate. If specified, `num` must be None.
  1913. Returns
  1914. -------
  1915. random_rotation : `Rotation` instance
  1916. Contains a single rotation if `num` is None. Otherwise contains a
  1917. stack of `num` rotations.
  1918. Notes
  1919. -----
  1920. This function is optimized for efficiently sampling random rotation
  1921. matrices in three dimensions. For generating random rotation matrices
  1922. in higher dimensions, see `scipy.stats.special_ortho_group`.
  1923. Examples
  1924. --------
  1925. >>> from scipy.spatial.transform import Rotation as R
  1926. Sample a single rotation:
  1927. >>> R.random().as_euler('zxy', degrees=True)
  1928. array([-110.5976185 , 55.32758512, 76.3289269 ]) # random
  1929. Sample a stack of rotations:
  1930. >>> R.random(5).as_euler('zxy', degrees=True)
  1931. array([[-110.5976185 , 55.32758512, 76.3289269 ], # random
  1932. [ -91.59132005, -14.3629884 , -93.91933182],
  1933. [ 25.23835501, 45.02035145, -121.67867086],
  1934. [ -51.51414184, -15.29022692, -172.46870023],
  1935. [ -81.63376847, -27.39521579, 2.60408416]])
  1936. See Also
  1937. --------
  1938. scipy.stats.special_ortho_group
  1939. """
  1940. # TODO: We should move to one single way of specifying the output shape and
  1941. # deprecate `num`.
  1942. if num is not None and shape is not None:
  1943. raise ValueError("Only one of `num` or `shape` can be specified.")
  1944. sample = cython_backend.random(num, rng, shape=shape)
  1945. return Rotation(sample, normalize=True, copy=False)
  1946. @staticmethod
  1947. @xp_capabilities(
  1948. skip_backends=[
  1949. ("dask.array", "missing linalg.cross/det functions and .mT attribute"),
  1950. ("cupy", "missing .mT attribute in cupy<14.*"),
  1951. ]
  1952. )
  1953. def align_vectors(
  1954. a: ArrayLike,
  1955. b: ArrayLike,
  1956. weights: ArrayLike | None = None,
  1957. return_sensitivity: bool = False,
  1958. ) -> tuple[Rotation, float] | tuple[Rotation, float, Array]:
  1959. """Estimate a rotation to optimally align two sets of vectors.
  1960. Find a rotation between frames A and B which best aligns a set of
  1961. vectors `a` and `b` observed in these frames. The following loss
  1962. function is minimized to solve for the rotation matrix
  1963. :math:`C`:
  1964. .. math::
  1965. L(C) = \\frac{1}{2} \\sum_{i = 1}^{n} w_i \\lVert \\mathbf{a}_i -
  1966. C \\mathbf{b}_i \\rVert^2 ,
  1967. where :math:`w_i`'s are the `weights` corresponding to each vector.
  1968. The rotation is estimated with Kabsch algorithm [1]_, and solves what
  1969. is known as the "pointing problem", or "Wahba's problem" [2]_.
  1970. Note that the length of each vector in this formulation acts as an
  1971. implicit weight. So for use cases where all vectors need to be
  1972. weighted equally, you should normalize them to unit length prior to
  1973. calling this method.
  1974. There are two special cases. The first is if a single vector is given
  1975. for `a` and `b`, in which the shortest distance rotation that aligns
  1976. `b` to `a` is returned.
  1977. The second is when one of the weights is infinity. In this case, the
  1978. shortest distance rotation between the primary infinite weight vectors
  1979. is calculated as above. Then, the rotation about the aligned primary
  1980. vectors is calculated such that the secondary vectors are optimally
  1981. aligned per the above loss function. The result is the composition
  1982. of these two rotations. The result via this process is the same as the
  1983. Kabsch algorithm as the corresponding weight approaches infinity in
  1984. the limit. For a single secondary vector this is known as the
  1985. "align-constrain" algorithm [3]_.
  1986. For both special cases (single vectors or an infinite weight), the
  1987. sensitivity matrix does not have physical meaning and an error will be
  1988. raised if it is requested. For an infinite weight, the primary vectors
  1989. act as a constraint with perfect alignment, so their contribution to
  1990. `rssd` will be forced to 0 even if they are of different lengths.
  1991. Parameters
  1992. ----------
  1993. a : array_like, shape (3,) or (N, 3)
  1994. Vector components observed in initial frame A. Each row of `a`
  1995. denotes a vector.
  1996. b : array_like, shape (3,) or (N, 3)
  1997. Vector components observed in another frame B. Each row of `b`
  1998. denotes a vector.
  1999. weights : array_like shape (N,), optional
  2000. Weights describing the relative importance of the vector
  2001. observations. If None (default), then all values in `weights` are
  2002. assumed to be 1. One and only one weight may be infinity, and
  2003. weights must be positive.
  2004. return_sensitivity : bool, optional
  2005. Whether to return the sensitivity matrix. See Notes for details.
  2006. Default is False.
  2007. Returns
  2008. -------
  2009. rotation : `Rotation` instance
  2010. Best estimate of the rotation that transforms `b` to `a`.
  2011. rssd : float
  2012. Stands for "root sum squared distance". Square root of the weighted
  2013. sum of the squared distances between the given sets of vectors
  2014. after alignment. It is equal to ``sqrt(2 * minimum_loss)``, where
  2015. ``minimum_loss`` is the loss function evaluated for the found
  2016. optimal rotation.
  2017. Note that the result will also be weighted by the vectors'
  2018. magnitudes, so perfectly aligned vector pairs will have nonzero
  2019. `rssd` if they are not of the same length. This can be avoided by
  2020. normalizing them to unit length prior to calling this method,
  2021. though note that doing this will change the resulting rotation.
  2022. sensitivity_matrix : ndarray, shape (3, 3)
  2023. Sensitivity matrix of the estimated rotation estimate as explained
  2024. in Notes. Returned only when `return_sensitivity` is True. Not
  2025. valid if aligning a single pair of vectors or if there is an
  2026. infinite weight, in which cases an error will be raised.
  2027. Notes
  2028. -----
  2029. The sensitivity matrix gives the sensitivity of the estimated rotation
  2030. to small perturbations of the vector measurements. Specifically we
  2031. consider the rotation estimate error as a small rotation vector of
  2032. frame A. The sensitivity matrix is proportional to the covariance of
  2033. this rotation vector assuming that the vectors in `a` was measured with
  2034. errors significantly less than their lengths. To get the true
  2035. covariance matrix, the returned sensitivity matrix must be multiplied
  2036. by harmonic mean [4]_ of variance in each observation. Note that
  2037. `weights` are supposed to be inversely proportional to the observation
  2038. variances to get consistent results. For example, if all vectors are
  2039. measured with the same accuracy of 0.01 (`weights` must be all equal),
  2040. then you should multiple the sensitivity matrix by 0.01**2 to get the
  2041. covariance.
  2042. Refer to [5]_ for more rigorous discussion of the covariance
  2043. estimation. See [6]_ for more discussion of the pointing problem and
  2044. minimal proper pointing.
  2045. This function does not support broadcasting or ND arrays with N > 2.
  2046. References
  2047. ----------
  2048. .. [1] https://en.wikipedia.org/wiki/Kabsch_algorithm
  2049. .. [2] https://en.wikipedia.org/wiki/Wahba%27s_problem
  2050. .. [3] Magner, Robert,
  2051. "Extending target tracking capabilities through trajectory and
  2052. momentum setpoint optimization." Small Satellite Conference,
  2053. 2018.
  2054. .. [4] https://en.wikipedia.org/wiki/Harmonic_mean
  2055. .. [5] F. Landis Markley,
  2056. "Attitude determination using vector observations: a fast
  2057. optimal matrix algorithm", Journal of Astronautical Sciences,
  2058. Vol. 41, No.2, 1993, pp. 261-280.
  2059. .. [6] Bar-Itzhack, Itzhack Y., Daniel Hershkowitz, and Leiba Rodman,
  2060. "Pointing in Real Euclidean Space", Journal of Guidance,
  2061. Control, and Dynamics, Vol. 20, No. 5, 1997, pp. 916-922.
  2062. Examples
  2063. --------
  2064. >>> import numpy as np
  2065. >>> from scipy.spatial.transform import Rotation as R
  2066. Here we run the baseline Kabsch algorithm to best align two sets of
  2067. vectors, where there is noise on the last two vector measurements of
  2068. the ``b`` set:
  2069. >>> a = [[0, 1, 0], [0, 1, 1], [0, 1, 1]]
  2070. >>> b = [[1, 0, 0], [1, 1.1, 0], [1, 0.9, 0]]
  2071. >>> rot, rssd, sens = R.align_vectors(a, b, return_sensitivity=True)
  2072. >>> rot.as_matrix()
  2073. array([[0., 0., 1.],
  2074. [1., 0., 0.],
  2075. [0., 1., 0.]])
  2076. When we apply the rotation to ``b``, we get vectors close to ``a``:
  2077. >>> rot.apply(b)
  2078. array([[0. , 1. , 0. ],
  2079. [0. , 1. , 1.1],
  2080. [0. , 1. , 0.9]])
  2081. The error for the first vector is 0, and for the last two the error is
  2082. magnitude 0.1. The `rssd` is the square root of the sum of the
  2083. weighted squared errors, and the default weights are all 1, so in this
  2084. case the `rssd` is calculated as
  2085. ``sqrt(1 * 0**2 + 1 * 0.1**2 + 1 * (-0.1)**2) = 0.141421356237308``
  2086. >>> a - rot.apply(b)
  2087. array([[ 0., 0., 0. ],
  2088. [ 0., 0., -0.1],
  2089. [ 0., 0., 0.1]])
  2090. >>> np.sqrt(np.sum(np.ones(3) @ (a - rot.apply(b))**2))
  2091. 0.141421356237308
  2092. >>> rssd
  2093. 0.141421356237308
  2094. The sensitivity matrix for this example is as follows:
  2095. >>> sens
  2096. array([[0.2, 0. , 0.],
  2097. [0. , 1.5, 1.],
  2098. [0. , 1. , 1.]])
  2099. Special case 1: Find a minimum rotation between single vectors:
  2100. >>> a = [1, 0, 0]
  2101. >>> b = [0, 1, 0]
  2102. >>> rot, _ = R.align_vectors(a, b)
  2103. >>> rot.as_matrix()
  2104. array([[0., 1., 0.],
  2105. [-1., 0., 0.],
  2106. [0., 0., 1.]])
  2107. >>> rot.apply(b)
  2108. array([1., 0., 0.])
  2109. Special case 2: One infinite weight. Here we find a rotation between
  2110. primary and secondary vectors that can align exactly:
  2111. >>> a = [[0, 1, 0], [0, 1, 1]]
  2112. >>> b = [[1, 0, 0], [1, 1, 0]]
  2113. >>> rot, _ = R.align_vectors(a, b, weights=[np.inf, 1])
  2114. >>> rot.as_matrix()
  2115. array([[0., 0., 1.],
  2116. [1., 0., 0.],
  2117. [0., 1., 0.]])
  2118. >>> rot.apply(b)
  2119. array([[0., 1., 0.],
  2120. [0., 1., 1.]])
  2121. Here the secondary vectors must be best-fit:
  2122. >>> a = [[0, 1, 0], [0, 1, 1]]
  2123. >>> b = [[1, 0, 0], [1, 2, 0]]
  2124. >>> rot, _ = R.align_vectors(a, b, weights=[np.inf, 1])
  2125. >>> rot.as_matrix()
  2126. array([[0., 0., 1.],
  2127. [1., 0., 0.],
  2128. [0., 1., 0.]])
  2129. >>> rot.apply(b)
  2130. array([[0., 1., 0.],
  2131. [0., 1., 2.]])
  2132. """
  2133. xp = array_namespace(a)
  2134. a, b, weights = _promote(a, b, weights, xp=xp)
  2135. cython_compatible = (
  2136. (a.ndim < 3) & (b.ndim < 3) & (weights is None or weights.ndim < 2)
  2137. )
  2138. backend = select_backend(xp, cython_compatible=cython_compatible)
  2139. q, rssd, sensitivity = backend.align_vectors(a, b, weights, return_sensitivity)
  2140. if return_sensitivity:
  2141. return Rotation._from_raw_quat(q, xp=xp, backend=backend), rssd, sensitivity
  2142. return Rotation._from_raw_quat(q, xp=xp, backend=backend), rssd
  2143. def __getstate__(self) -> tuple[Array, bool]:
  2144. return (self._quat, self._single)
  2145. def __setstate__(self, state: tuple[Array, bool]):
  2146. quat, single = state
  2147. xp = array_namespace(quat)
  2148. self._xp = xp
  2149. self._quat = xp.asarray(quat, copy=True)
  2150. self._backend = select_backend(xp, cython_compatible=self._quat.ndim < 3)
  2151. self._single = single
  2152. @property
  2153. def single(self) -> bool:
  2154. """Whether this instance represents a single rotation."""
  2155. return self._single or self._quat.ndim == 1
  2156. @property
  2157. def shape(self) -> tuple[int, ...]:
  2158. """The shape of the rotation's leading dimensions."""
  2159. if self._single:
  2160. return ()
  2161. return self._quat.shape[:-1]
  2162. def __bool__(self) -> bool:
  2163. """Comply with Python convention for objects to be True.
  2164. Required because `Rotation.__len__()` is defined and not always truthy.
  2165. """
  2166. return True
  2167. def __len__(self) -> int:
  2168. """Number of rotations contained in this object.
  2169. Multiple rotations can be stored in a single instance.
  2170. Returns
  2171. -------
  2172. length : int
  2173. Number of rotations stored in object.
  2174. Raises
  2175. ------
  2176. TypeError if the instance was created as a single rotation.
  2177. """
  2178. if self._single or self._quat.ndim == 1:
  2179. raise TypeError("Single rotation has no len().")
  2180. return self._quat.shape[0]
  2181. @xp_capabilities(
  2182. skip_backends=[("dask.array", "missing linalg.cross/det functions")]
  2183. )
  2184. def __repr__(self) -> str:
  2185. m = f"{self.as_matrix()!r}".splitlines()
  2186. # bump indent (+21 characters)
  2187. m[1:] = [" " * 21 + m[i] for i in range(1, len(m))]
  2188. return "Rotation.from_matrix(" + "\n".join(m) + ")"
  2189. @xp_capabilities()
  2190. def __iter__(self) -> Iterator[Rotation]:
  2191. """Iterate over rotations."""
  2192. if self._single or self._quat.ndim == 1:
  2193. raise TypeError("Single rotation is not iterable.")
  2194. # We return a generator that yields a new Rotation object for each rotation
  2195. # in the current object. We cannot rely on the default implementation using
  2196. # __getitem__ because jax will not raise an IndexError for out-of-bounds
  2197. # indices.
  2198. for i in range(self._quat.shape[0]):
  2199. yield Rotation(self._quat[i, ...], normalize=False, copy=False)
  2200. @staticmethod
  2201. def _from_raw_quat(
  2202. quat: Array, xp: ModuleType, backend: ModuleType | None = None
  2203. ) -> Rotation:
  2204. """Create a Rotation skipping all sanitization steps.
  2205. This method is intended for internal, performant creation of Rotations with
  2206. quaternions that are guaranteed to be valid.
  2207. """
  2208. rot = Rotation.__new__(Rotation)
  2209. rot._single = quat.ndim == 1 and is_numpy(xp)
  2210. if rot._single:
  2211. quat = xpx.atleast_nd(quat, ndim=2, xp=xp)
  2212. rot._quat = quat
  2213. rot._xp = xp
  2214. if backend is None:
  2215. backend = select_backend(xp, cython_compatible=quat.ndim < 3)
  2216. rot._backend = backend
  2217. return rot
  2218. class Slerp:
  2219. """Spherical Linear Interpolation of Rotations.
  2220. The interpolation between consecutive rotations is performed as a rotation
  2221. around a fixed axis with a constant angular velocity [1]_. This ensures
  2222. that the interpolated rotations follow the shortest path between initial
  2223. and final orientations.
  2224. Parameters
  2225. ----------
  2226. times : array_like, shape (N,)
  2227. Times of the known rotations. At least 2 times must be specified.
  2228. rotations : `Rotation` instance
  2229. Rotations to perform the interpolation between. Must contain N
  2230. rotations.
  2231. Methods
  2232. -------
  2233. __call__
  2234. See Also
  2235. --------
  2236. Rotation
  2237. Notes
  2238. -----
  2239. This class only supports interpolation of rotations with a single leading
  2240. dimension.
  2241. .. versionadded:: 1.2.0
  2242. References
  2243. ----------
  2244. .. [1] https://en.wikipedia.org/wiki/Slerp#Quaternion_Slerp
  2245. Examples
  2246. --------
  2247. >>> from scipy.spatial.transform import Rotation as R
  2248. >>> from scipy.spatial.transform import Slerp
  2249. Setup the fixed keyframe rotations and times:
  2250. >>> key_rots = R.random(5, random_state=2342345)
  2251. >>> key_times = [0, 1, 2, 3, 4]
  2252. Create the interpolator object:
  2253. >>> slerp = Slerp(key_times, key_rots)
  2254. Interpolate the rotations at the given times:
  2255. >>> times = [0, 0.5, 0.25, 1, 1.5, 2, 2.75, 3, 3.25, 3.60, 4]
  2256. >>> interp_rots = slerp(times)
  2257. The keyframe rotations expressed as Euler angles:
  2258. >>> key_rots.as_euler('xyz', degrees=True)
  2259. array([[ 14.31443779, -27.50095894, -3.7275787 ],
  2260. [ -1.79924227, -24.69421529, 164.57701743],
  2261. [146.15020772, 43.22849451, -31.34891088],
  2262. [ 46.39959442, 11.62126073, -45.99719267],
  2263. [-88.94647804, -49.64400082, -65.80546984]])
  2264. The interpolated rotations expressed as Euler angles. These agree with the
  2265. keyframe rotations at both endpoints of the range of keyframe times.
  2266. >>> interp_rots.as_euler('xyz', degrees=True)
  2267. array([[ 14.31443779, -27.50095894, -3.7275787 ],
  2268. [ 4.74588574, -32.44683966, 81.25139984],
  2269. [ 10.71094749, -31.56690154, 38.06896408],
  2270. [ -1.79924227, -24.69421529, 164.57701743],
  2271. [ 11.72796022, 51.64207311, -171.7374683 ],
  2272. [ 146.15020772, 43.22849451, -31.34891088],
  2273. [ 68.10921869, 20.67625074, -48.74886034],
  2274. [ 46.39959442, 11.62126073, -45.99719267],
  2275. [ 12.35552615, 4.21525086, -64.89288124],
  2276. [ -30.08117143, -19.90769513, -78.98121326],
  2277. [ -88.94647804, -49.64400082, -65.80546984]])
  2278. """
  2279. @xp_capabilities(
  2280. jax_jit=False, skip_backends=[("dask.array", "missing linalg.cross function")]
  2281. )
  2282. def __init__(self, times: ArrayLike, rotations: Rotation):
  2283. if not isinstance(rotations, Rotation):
  2284. raise TypeError("`rotations` must be a `Rotation` instance.")
  2285. if rotations.single or len(rotations) <= 1:
  2286. raise ValueError("`rotations` must be a sequence of at least 2 rotations.")
  2287. q = rotations.as_quat()
  2288. if q.ndim > 2:
  2289. raise ValueError(
  2290. "Rotations with more than 1 leading dimension are not supported."
  2291. )
  2292. xp = array_namespace(q)
  2293. times = xp.asarray(times, device=xp_device(q), dtype=q.dtype)
  2294. if times.ndim != 1:
  2295. raise ValueError(
  2296. "Expected times to be specified in a 1 dimensional array, got "
  2297. f"{times.ndim} dimensions."
  2298. )
  2299. if times.shape[0] != len(rotations):
  2300. raise ValueError(
  2301. "Expected number of rotations to be equal to number of timestamps "
  2302. f"given, got {len(rotations)} rotations and {times.shape[0]} "
  2303. "timestamps."
  2304. )
  2305. self.times = times
  2306. self.timedelta = xp.diff(times)
  2307. # We cannot check for values for lazy backends, so we cannot raise an error on
  2308. # timedelta < 0 for lazy backends. Instead, we set timedelta to nans
  2309. neg_mask = xp.any(self.timedelta <= 0)
  2310. if is_lazy_array(neg_mask):
  2311. self.timedelta = xp.where(neg_mask, xp.nan, self.timedelta)
  2312. self.times = xp.where(neg_mask, xp.nan, self.times)
  2313. elif xp.any(neg_mask):
  2314. raise ValueError("Times must be in strictly increasing order.")
  2315. self.rotations = rotations[:-1]
  2316. self.rotvecs = (self.rotations.inv() * rotations[1:]).as_rotvec()
  2317. @xp_capabilities()
  2318. def __call__(self, times: ArrayLike) -> Rotation:
  2319. """Interpolate rotations.
  2320. Compute the interpolated rotations at the given `times`.
  2321. Parameters
  2322. ----------
  2323. times : array_like
  2324. Times to compute the interpolations at. Can be a scalar or
  2325. 1-dimensional.
  2326. Returns
  2327. -------
  2328. interpolated_rotation : `Rotation` instance
  2329. Object containing the rotations computed at given `times`.
  2330. """
  2331. xp = array_namespace(self.times)
  2332. device = xp_device(self.times)
  2333. # Clearly differentiate from self.times property
  2334. compute_times = xp.asarray(times, device=device, dtype=self.times.dtype)
  2335. if compute_times.ndim > 1:
  2336. raise ValueError("`times` must be at most 1-dimensional.")
  2337. single_time = compute_times.ndim == 0
  2338. compute_times = xpx.atleast_nd(compute_times, ndim=1, xp=xp)
  2339. # side = 'left' (default) excludes t_min.
  2340. ind = xp.searchsorted(self.times, compute_times) - 1
  2341. # Include t_min. Without this step, index for t_min equals -1
  2342. ind = xpx.at(ind, compute_times == self.times[0]).set(0)
  2343. # We cannot error out on invalid indices for jit compiled code. To not produce
  2344. # an index error, we set the index to 0 in case it is out of bounds, and later
  2345. # set the result to nan.
  2346. invalid_ind = (ind < 0) | (ind > len(self.rotations) - 1)
  2347. if is_lazy_array(invalid_ind):
  2348. ind = xpx.at(ind, invalid_ind).set(0)
  2349. elif xp.any(invalid_ind):
  2350. raise ValueError(
  2351. f"Interpolation times must be within the range [{self.times[0]}, "
  2352. f"{self.times[-1]}], both inclusive."
  2353. )
  2354. alpha = (compute_times - self.times[ind]) / self.timedelta[ind]
  2355. alpha = xpx.at(alpha, invalid_ind).set(xp.nan)
  2356. # The array API does not support integer arrays + ellipsis indexing and won't
  2357. # stabilize this feature due to blockers in PyTorch. Therefore we need to
  2358. # construct the index for the last dimension manually.
  2359. # See https://github.com/scipy/scipy/pull/23249#discussion_r2198363047
  2360. result = self.rotations[ind] * Rotation.from_rotvec(
  2361. self.rotvecs[ind[:, None], xp.arange(3, device=device)] * alpha[:, None]
  2362. )
  2363. if single_time:
  2364. result = result[0]
  2365. return result