scipy 1.15.3__cp313-cp313-musllinux_1_2_aarch64.whl → 1.16.0__cp313-cp313-musllinux_1_2_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (642) hide show
  1. scipy/__config__.py +11 -11
  2. scipy/__init__.py +3 -6
  3. scipy/_cyutility.cpython-313-aarch64-linux-musl.so +0 -0
  4. scipy/_lib/_array_api.py +486 -161
  5. scipy/_lib/_array_api_compat_vendor.py +9 -0
  6. scipy/_lib/_bunch.py +4 -0
  7. scipy/_lib/_ccallback_c.cpython-313-aarch64-linux-musl.so +0 -0
  8. scipy/_lib/_docscrape.py +1 -1
  9. scipy/_lib/_elementwise_iterative_method.py +15 -26
  10. scipy/_lib/_fpumode.cpython-313-aarch64-linux-musl.so +0 -0
  11. scipy/_lib/_sparse.py +41 -0
  12. scipy/_lib/_test_ccallback.cpython-313-aarch64-linux-musl.so +0 -0
  13. scipy/_lib/_test_deprecation_call.cpython-313-aarch64-linux-musl.so +0 -0
  14. scipy/_lib/_test_deprecation_def.cpython-313-aarch64-linux-musl.so +0 -0
  15. scipy/_lib/_testutils.py +6 -2
  16. scipy/_lib/_uarray/_uarray.cpython-313-aarch64-linux-musl.so +0 -0
  17. scipy/_lib/_util.py +222 -125
  18. scipy/_lib/array_api_compat/__init__.py +4 -4
  19. scipy/_lib/array_api_compat/_internal.py +19 -6
  20. scipy/_lib/array_api_compat/common/__init__.py +1 -1
  21. scipy/_lib/array_api_compat/common/_aliases.py +365 -193
  22. scipy/_lib/array_api_compat/common/_fft.py +94 -64
  23. scipy/_lib/array_api_compat/common/_helpers.py +413 -180
  24. scipy/_lib/array_api_compat/common/_linalg.py +116 -40
  25. scipy/_lib/array_api_compat/common/_typing.py +179 -10
  26. scipy/_lib/array_api_compat/cupy/__init__.py +1 -4
  27. scipy/_lib/array_api_compat/cupy/_aliases.py +61 -41
  28. scipy/_lib/array_api_compat/cupy/_info.py +16 -6
  29. scipy/_lib/array_api_compat/cupy/_typing.py +24 -39
  30. scipy/_lib/array_api_compat/dask/array/__init__.py +6 -3
  31. scipy/_lib/array_api_compat/dask/array/_aliases.py +267 -108
  32. scipy/_lib/array_api_compat/dask/array/_info.py +105 -34
  33. scipy/_lib/array_api_compat/dask/array/fft.py +5 -8
  34. scipy/_lib/array_api_compat/dask/array/linalg.py +21 -22
  35. scipy/_lib/array_api_compat/numpy/__init__.py +13 -15
  36. scipy/_lib/array_api_compat/numpy/_aliases.py +98 -49
  37. scipy/_lib/array_api_compat/numpy/_info.py +36 -16
  38. scipy/_lib/array_api_compat/numpy/_typing.py +27 -43
  39. scipy/_lib/array_api_compat/numpy/fft.py +11 -5
  40. scipy/_lib/array_api_compat/numpy/linalg.py +75 -22
  41. scipy/_lib/array_api_compat/torch/__init__.py +3 -5
  42. scipy/_lib/array_api_compat/torch/_aliases.py +262 -159
  43. scipy/_lib/array_api_compat/torch/_info.py +27 -16
  44. scipy/_lib/array_api_compat/torch/_typing.py +3 -0
  45. scipy/_lib/array_api_compat/torch/fft.py +17 -18
  46. scipy/_lib/array_api_compat/torch/linalg.py +16 -16
  47. scipy/_lib/array_api_extra/__init__.py +26 -3
  48. scipy/_lib/array_api_extra/_delegation.py +171 -0
  49. scipy/_lib/array_api_extra/_lib/__init__.py +1 -0
  50. scipy/_lib/array_api_extra/_lib/_at.py +463 -0
  51. scipy/_lib/array_api_extra/_lib/_backends.py +46 -0
  52. scipy/_lib/array_api_extra/_lib/_funcs.py +937 -0
  53. scipy/_lib/array_api_extra/_lib/_lazy.py +357 -0
  54. scipy/_lib/array_api_extra/_lib/_testing.py +278 -0
  55. scipy/_lib/array_api_extra/_lib/_utils/__init__.py +1 -0
  56. scipy/_lib/array_api_extra/_lib/_utils/_compat.py +74 -0
  57. scipy/_lib/array_api_extra/_lib/_utils/_compat.pyi +45 -0
  58. scipy/_lib/array_api_extra/_lib/_utils/_helpers.py +559 -0
  59. scipy/_lib/array_api_extra/_lib/_utils/_typing.py +10 -0
  60. scipy/_lib/array_api_extra/_lib/_utils/_typing.pyi +105 -0
  61. scipy/_lib/array_api_extra/testing.py +359 -0
  62. scipy/_lib/decorator.py +2 -2
  63. scipy/_lib/doccer.py +1 -7
  64. scipy/_lib/messagestream.cpython-313-aarch64-linux-musl.so +0 -0
  65. scipy/_lib/pyprima/__init__.py +212 -0
  66. scipy/_lib/pyprima/cobyla/__init__.py +0 -0
  67. scipy/_lib/pyprima/cobyla/cobyla.py +559 -0
  68. scipy/_lib/pyprima/cobyla/cobylb.py +714 -0
  69. scipy/_lib/pyprima/cobyla/geometry.py +226 -0
  70. scipy/_lib/pyprima/cobyla/initialize.py +215 -0
  71. scipy/_lib/pyprima/cobyla/trustregion.py +492 -0
  72. scipy/_lib/pyprima/cobyla/update.py +289 -0
  73. scipy/_lib/pyprima/common/__init__.py +0 -0
  74. scipy/_lib/pyprima/common/_bounds.py +34 -0
  75. scipy/_lib/pyprima/common/_linear_constraints.py +46 -0
  76. scipy/_lib/pyprima/common/_nonlinear_constraints.py +54 -0
  77. scipy/_lib/pyprima/common/_project.py +173 -0
  78. scipy/_lib/pyprima/common/checkbreak.py +93 -0
  79. scipy/_lib/pyprima/common/consts.py +47 -0
  80. scipy/_lib/pyprima/common/evaluate.py +99 -0
  81. scipy/_lib/pyprima/common/history.py +38 -0
  82. scipy/_lib/pyprima/common/infos.py +30 -0
  83. scipy/_lib/pyprima/common/linalg.py +435 -0
  84. scipy/_lib/pyprima/common/message.py +290 -0
  85. scipy/_lib/pyprima/common/powalg.py +131 -0
  86. scipy/_lib/pyprima/common/preproc.py +277 -0
  87. scipy/_lib/pyprima/common/present.py +5 -0
  88. scipy/_lib/pyprima/common/ratio.py +54 -0
  89. scipy/_lib/pyprima/common/redrho.py +47 -0
  90. scipy/_lib/pyprima/common/selectx.py +296 -0
  91. scipy/_lib/tests/test__util.py +105 -121
  92. scipy/_lib/tests/test_array_api.py +166 -35
  93. scipy/_lib/tests/test_bunch.py +7 -0
  94. scipy/_lib/tests/test_ccallback.py +2 -10
  95. scipy/_lib/tests/test_public_api.py +13 -0
  96. scipy/cluster/_hierarchy.cpython-313-aarch64-linux-musl.so +0 -0
  97. scipy/cluster/_optimal_leaf_ordering.cpython-313-aarch64-linux-musl.so +0 -0
  98. scipy/cluster/_vq.cpython-313-aarch64-linux-musl.so +0 -0
  99. scipy/cluster/hierarchy.py +393 -223
  100. scipy/cluster/tests/test_hierarchy.py +273 -335
  101. scipy/cluster/tests/test_vq.py +45 -61
  102. scipy/cluster/vq.py +39 -35
  103. scipy/conftest.py +282 -151
  104. scipy/constants/_constants.py +4 -1
  105. scipy/constants/tests/test_codata.py +2 -2
  106. scipy/constants/tests/test_constants.py +11 -18
  107. scipy/datasets/_download_all.py +15 -1
  108. scipy/datasets/_fetchers.py +7 -1
  109. scipy/datasets/_utils.py +1 -1
  110. scipy/differentiate/_differentiate.py +25 -25
  111. scipy/differentiate/tests/test_differentiate.py +24 -25
  112. scipy/fft/_basic.py +20 -0
  113. scipy/fft/_helper.py +3 -34
  114. scipy/fft/_pocketfft/helper.py +29 -1
  115. scipy/fft/_pocketfft/pypocketfft.cpython-313-aarch64-linux-musl.so +0 -0
  116. scipy/fft/_pocketfft/tests/test_basic.py +2 -4
  117. scipy/fft/_pocketfft/tests/test_real_transforms.py +4 -4
  118. scipy/fft/_realtransforms.py +13 -0
  119. scipy/fft/tests/test_basic.py +27 -25
  120. scipy/fft/tests/test_fftlog.py +16 -7
  121. scipy/fft/tests/test_helper.py +18 -34
  122. scipy/fft/tests/test_real_transforms.py +8 -10
  123. scipy/fftpack/convolve.cpython-313-aarch64-linux-musl.so +0 -0
  124. scipy/fftpack/tests/test_basic.py +2 -4
  125. scipy/fftpack/tests/test_real_transforms.py +8 -9
  126. scipy/integrate/_bvp.py +9 -3
  127. scipy/integrate/_cubature.py +3 -2
  128. scipy/integrate/_dop.cpython-313-aarch64-linux-musl.so +0 -0
  129. scipy/integrate/_lsoda.cpython-313-aarch64-linux-musl.so +0 -0
  130. scipy/integrate/_ode.py +9 -2
  131. scipy/integrate/_odepack.cpython-313-aarch64-linux-musl.so +0 -0
  132. scipy/integrate/_quad_vec.py +21 -29
  133. scipy/integrate/_quadpack.cpython-313-aarch64-linux-musl.so +0 -0
  134. scipy/integrate/_quadpack_py.py +11 -7
  135. scipy/integrate/_quadrature.py +3 -3
  136. scipy/integrate/_rules/_base.py +2 -2
  137. scipy/integrate/_tanhsinh.py +48 -47
  138. scipy/integrate/_test_multivariate.cpython-313-aarch64-linux-musl.so +0 -0
  139. scipy/integrate/_test_odeint_banded.cpython-313-aarch64-linux-musl.so +0 -0
  140. scipy/integrate/_vode.cpython-313-aarch64-linux-musl.so +0 -0
  141. scipy/integrate/tests/test__quad_vec.py +0 -6
  142. scipy/integrate/tests/test_banded_ode_solvers.py +85 -0
  143. scipy/integrate/tests/test_cubature.py +21 -35
  144. scipy/integrate/tests/test_quadrature.py +6 -8
  145. scipy/integrate/tests/test_tanhsinh.py +56 -48
  146. scipy/interpolate/__init__.py +70 -58
  147. scipy/interpolate/_bary_rational.py +22 -22
  148. scipy/interpolate/_bsplines.py +119 -66
  149. scipy/interpolate/_cubic.py +65 -50
  150. scipy/interpolate/_dfitpack.cpython-313-aarch64-linux-musl.so +0 -0
  151. scipy/interpolate/_dierckx.cpython-313-aarch64-linux-musl.so +0 -0
  152. scipy/interpolate/_fitpack.cpython-313-aarch64-linux-musl.so +0 -0
  153. scipy/interpolate/_fitpack2.py +9 -6
  154. scipy/interpolate/_fitpack_impl.py +32 -26
  155. scipy/interpolate/_fitpack_repro.py +23 -19
  156. scipy/interpolate/_interpnd.cpython-313-aarch64-linux-musl.so +0 -0
  157. scipy/interpolate/_interpolate.py +30 -12
  158. scipy/interpolate/_ndbspline.py +13 -18
  159. scipy/interpolate/_ndgriddata.py +5 -8
  160. scipy/interpolate/_polyint.py +95 -31
  161. scipy/interpolate/_ppoly.cpython-313-aarch64-linux-musl.so +0 -0
  162. scipy/interpolate/_rbf.py +2 -2
  163. scipy/interpolate/_rbfinterp.py +1 -1
  164. scipy/interpolate/_rbfinterp_pythran.cpython-313-aarch64-linux-musl.so +0 -0
  165. scipy/interpolate/_rgi.py +31 -26
  166. scipy/interpolate/_rgi_cython.cpython-313-aarch64-linux-musl.so +0 -0
  167. scipy/interpolate/dfitpack.py +0 -20
  168. scipy/interpolate/interpnd.py +1 -2
  169. scipy/interpolate/tests/test_bary_rational.py +2 -2
  170. scipy/interpolate/tests/test_bsplines.py +97 -1
  171. scipy/interpolate/tests/test_fitpack2.py +39 -1
  172. scipy/interpolate/tests/test_interpnd.py +32 -20
  173. scipy/interpolate/tests/test_interpolate.py +48 -4
  174. scipy/interpolate/tests/test_rgi.py +2 -1
  175. scipy/io/_fast_matrix_market/__init__.py +2 -0
  176. scipy/io/_fast_matrix_market/_fmm_core.cpython-313-aarch64-linux-musl.so +0 -0
  177. scipy/io/_harwell_boeing/_fortran_format_parser.py +19 -16
  178. scipy/io/_harwell_boeing/hb.py +7 -11
  179. scipy/io/_idl.py +5 -7
  180. scipy/io/_netcdf.py +15 -5
  181. scipy/io/_test_fortran.cpython-313-aarch64-linux-musl.so +0 -0
  182. scipy/io/arff/tests/test_arffread.py +3 -3
  183. scipy/io/matlab/__init__.py +5 -3
  184. scipy/io/matlab/_mio.py +4 -1
  185. scipy/io/matlab/_mio5.py +19 -13
  186. scipy/io/matlab/_mio5_utils.cpython-313-aarch64-linux-musl.so +0 -0
  187. scipy/io/matlab/_mio_utils.cpython-313-aarch64-linux-musl.so +0 -0
  188. scipy/io/matlab/_miobase.py +4 -1
  189. scipy/io/matlab/_streams.cpython-313-aarch64-linux-musl.so +0 -0
  190. scipy/io/matlab/tests/test_mio.py +46 -18
  191. scipy/io/matlab/tests/test_mio_funcs.py +1 -1
  192. scipy/io/tests/test_mmio.py +7 -1
  193. scipy/io/tests/test_wavfile.py +41 -0
  194. scipy/io/wavfile.py +57 -10
  195. scipy/linalg/_basic.py +113 -86
  196. scipy/linalg/_cythonized_array_utils.cpython-313-aarch64-linux-musl.so +0 -0
  197. scipy/linalg/_decomp.py +22 -9
  198. scipy/linalg/_decomp_cholesky.py +28 -13
  199. scipy/linalg/_decomp_cossin.py +45 -30
  200. scipy/linalg/_decomp_interpolative.cpython-313-aarch64-linux-musl.so +0 -0
  201. scipy/linalg/_decomp_ldl.py +4 -1
  202. scipy/linalg/_decomp_lu.py +18 -6
  203. scipy/linalg/_decomp_lu_cython.cpython-313-aarch64-linux-musl.so +0 -0
  204. scipy/linalg/_decomp_polar.py +2 -0
  205. scipy/linalg/_decomp_qr.py +6 -2
  206. scipy/linalg/_decomp_qz.py +3 -0
  207. scipy/linalg/_decomp_schur.py +3 -1
  208. scipy/linalg/_decomp_svd.py +13 -2
  209. scipy/linalg/_decomp_update.cpython-313-aarch64-linux-musl.so +0 -0
  210. scipy/linalg/_expm_frechet.py +4 -0
  211. scipy/linalg/_fblas.cpython-313-aarch64-linux-musl.so +0 -0
  212. scipy/linalg/_flapack.cpython-313-aarch64-linux-musl.so +0 -0
  213. scipy/linalg/_linalg_pythran.cpython-313-aarch64-linux-musl.so +0 -0
  214. scipy/linalg/_matfuncs.py +187 -4
  215. scipy/linalg/_matfuncs_expm.cpython-313-aarch64-linux-musl.so +0 -0
  216. scipy/linalg/_matfuncs_schur_sqrtm.cpython-313-aarch64-linux-musl.so +0 -0
  217. scipy/linalg/_matfuncs_sqrtm.py +1 -99
  218. scipy/linalg/_matfuncs_sqrtm_triu.cpython-313-aarch64-linux-musl.so +0 -0
  219. scipy/linalg/_procrustes.py +2 -0
  220. scipy/linalg/_sketches.py +17 -6
  221. scipy/linalg/_solve_toeplitz.cpython-313-aarch64-linux-musl.so +0 -0
  222. scipy/linalg/_solvers.py +7 -2
  223. scipy/linalg/_special_matrices.py +26 -36
  224. scipy/linalg/blas.py +35 -24
  225. scipy/linalg/cython_blas.cpython-313-aarch64-linux-musl.so +0 -0
  226. scipy/linalg/cython_lapack.cpython-313-aarch64-linux-musl.so +0 -0
  227. scipy/linalg/lapack.py +22 -2
  228. scipy/linalg/tests/_cython_examples/meson.build +7 -0
  229. scipy/linalg/tests/test_basic.py +31 -16
  230. scipy/linalg/tests/test_batch.py +588 -0
  231. scipy/linalg/tests/test_cythonized_array_utils.py +0 -2
  232. scipy/linalg/tests/test_decomp.py +40 -3
  233. scipy/linalg/tests/test_decomp_cossin.py +14 -0
  234. scipy/linalg/tests/test_decomp_ldl.py +1 -1
  235. scipy/linalg/tests/test_lapack.py +115 -7
  236. scipy/linalg/tests/test_matfuncs.py +157 -102
  237. scipy/linalg/tests/test_procrustes.py +0 -7
  238. scipy/linalg/tests/test_solve_toeplitz.py +1 -1
  239. scipy/linalg/tests/test_special_matrices.py +1 -5
  240. scipy/ndimage/__init__.py +1 -0
  241. scipy/ndimage/_ctest.cpython-313-aarch64-linux-musl.so +0 -0
  242. scipy/ndimage/_cytest.cpython-313-aarch64-linux-musl.so +0 -0
  243. scipy/ndimage/_delegators.py +8 -2
  244. scipy/ndimage/_filters.py +453 -5
  245. scipy/ndimage/_interpolation.py +36 -6
  246. scipy/ndimage/_measurements.py +4 -2
  247. scipy/ndimage/_morphology.py +5 -0
  248. scipy/ndimage/_nd_image.cpython-313-aarch64-linux-musl.so +0 -0
  249. scipy/ndimage/_ni_docstrings.py +5 -1
  250. scipy/ndimage/_ni_label.cpython-313-aarch64-linux-musl.so +0 -0
  251. scipy/ndimage/_ni_support.py +1 -5
  252. scipy/ndimage/_rank_filter_1d.cpython-313-aarch64-linux-musl.so +0 -0
  253. scipy/ndimage/_support_alternative_backends.py +18 -6
  254. scipy/ndimage/tests/test_filters.py +370 -259
  255. scipy/ndimage/tests/test_fourier.py +7 -9
  256. scipy/ndimage/tests/test_interpolation.py +68 -61
  257. scipy/ndimage/tests/test_measurements.py +18 -35
  258. scipy/ndimage/tests/test_morphology.py +143 -131
  259. scipy/ndimage/tests/test_splines.py +1 -3
  260. scipy/odr/__odrpack.cpython-313-aarch64-linux-musl.so +0 -0
  261. scipy/optimize/_basinhopping.py +13 -7
  262. scipy/optimize/_bglu_dense.cpython-313-aarch64-linux-musl.so +0 -0
  263. scipy/optimize/_bracket.py +17 -24
  264. scipy/optimize/_chandrupatla.py +9 -10
  265. scipy/optimize/_cobyla_py.py +104 -123
  266. scipy/optimize/_constraints.py +14 -10
  267. scipy/optimize/_differentiable_functions.py +371 -230
  268. scipy/optimize/_differentialevolution.py +4 -3
  269. scipy/optimize/_direct.cpython-313-aarch64-linux-musl.so +0 -0
  270. scipy/optimize/_dual_annealing.py +1 -1
  271. scipy/optimize/_elementwise.py +1 -4
  272. scipy/optimize/_group_columns.cpython-313-aarch64-linux-musl.so +0 -0
  273. scipy/optimize/_highspy/_core.cpython-313-aarch64-linux-musl.so +0 -0
  274. scipy/optimize/_highspy/_highs_options.cpython-313-aarch64-linux-musl.so +0 -0
  275. scipy/optimize/_lbfgsb.cpython-313-aarch64-linux-musl.so +0 -0
  276. scipy/optimize/_lbfgsb_py.py +80 -24
  277. scipy/optimize/_linprog_doc.py +2 -2
  278. scipy/optimize/_linprog_highs.py +2 -2
  279. scipy/optimize/_linprog_ip.py +25 -10
  280. scipy/optimize/_linprog_util.py +14 -16
  281. scipy/optimize/_lsap.cpython-313-aarch64-linux-musl.so +0 -0
  282. scipy/optimize/_lsq/common.py +3 -3
  283. scipy/optimize/_lsq/dogbox.py +16 -2
  284. scipy/optimize/_lsq/givens_elimination.cpython-313-aarch64-linux-musl.so +0 -0
  285. scipy/optimize/_lsq/least_squares.py +198 -126
  286. scipy/optimize/_lsq/lsq_linear.py +6 -6
  287. scipy/optimize/_lsq/trf.py +35 -8
  288. scipy/optimize/_milp.py +3 -1
  289. scipy/optimize/_minimize.py +105 -36
  290. scipy/optimize/_minpack.cpython-313-aarch64-linux-musl.so +0 -0
  291. scipy/optimize/_minpack_py.py +21 -14
  292. scipy/optimize/_moduleTNC.cpython-313-aarch64-linux-musl.so +0 -0
  293. scipy/optimize/_nnls.py +20 -21
  294. scipy/optimize/_nonlin.py +34 -3
  295. scipy/optimize/_numdiff.py +288 -110
  296. scipy/optimize/_optimize.py +86 -48
  297. scipy/optimize/_pava_pybind.cpython-313-aarch64-linux-musl.so +0 -0
  298. scipy/optimize/_remove_redundancy.py +5 -5
  299. scipy/optimize/_root_scalar.py +1 -1
  300. scipy/optimize/_shgo.py +6 -0
  301. scipy/optimize/_shgo_lib/_complex.py +1 -1
  302. scipy/optimize/_slsqp_py.py +216 -124
  303. scipy/optimize/_slsqplib.cpython-313-aarch64-linux-musl.so +0 -0
  304. scipy/optimize/_spectral.py +1 -1
  305. scipy/optimize/_tnc.py +8 -1
  306. scipy/optimize/_trlib/_trlib.cpython-313-aarch64-linux-musl.so +0 -0
  307. scipy/optimize/_trustregion.py +20 -6
  308. scipy/optimize/_trustregion_constr/canonical_constraint.py +7 -7
  309. scipy/optimize/_trustregion_constr/equality_constrained_sqp.py +1 -1
  310. scipy/optimize/_trustregion_constr/minimize_trustregion_constr.py +11 -3
  311. scipy/optimize/_trustregion_constr/projections.py +12 -8
  312. scipy/optimize/_trustregion_constr/qp_subproblem.py +9 -9
  313. scipy/optimize/_trustregion_constr/tests/test_projections.py +7 -7
  314. scipy/optimize/_trustregion_constr/tests/test_qp_subproblem.py +77 -77
  315. scipy/optimize/_trustregion_constr/tr_interior_point.py +5 -5
  316. scipy/optimize/_trustregion_exact.py +0 -1
  317. scipy/optimize/_zeros.cpython-313-aarch64-linux-musl.so +0 -0
  318. scipy/optimize/_zeros_py.py +97 -17
  319. scipy/optimize/cython_optimize/_zeros.cpython-313-aarch64-linux-musl.so +0 -0
  320. scipy/optimize/slsqp.py +0 -1
  321. scipy/optimize/tests/test__basinhopping.py +1 -1
  322. scipy/optimize/tests/test__differential_evolution.py +4 -4
  323. scipy/optimize/tests/test__linprog_clean_inputs.py +5 -3
  324. scipy/optimize/tests/test__numdiff.py +66 -22
  325. scipy/optimize/tests/test__remove_redundancy.py +2 -2
  326. scipy/optimize/tests/test__shgo.py +9 -1
  327. scipy/optimize/tests/test_bracket.py +36 -46
  328. scipy/optimize/tests/test_chandrupatla.py +133 -135
  329. scipy/optimize/tests/test_cobyla.py +74 -45
  330. scipy/optimize/tests/test_constraints.py +1 -1
  331. scipy/optimize/tests/test_differentiable_functions.py +226 -6
  332. scipy/optimize/tests/test_lbfgsb_hessinv.py +22 -0
  333. scipy/optimize/tests/test_least_squares.py +125 -13
  334. scipy/optimize/tests/test_linear_assignment.py +3 -3
  335. scipy/optimize/tests/test_linprog.py +3 -3
  336. scipy/optimize/tests/test_lsq_linear.py +6 -6
  337. scipy/optimize/tests/test_minimize_constrained.py +2 -2
  338. scipy/optimize/tests/test_minpack.py +4 -4
  339. scipy/optimize/tests/test_nnls.py +43 -3
  340. scipy/optimize/tests/test_nonlin.py +36 -0
  341. scipy/optimize/tests/test_optimize.py +98 -20
  342. scipy/optimize/tests/test_slsqp.py +36 -4
  343. scipy/optimize/tests/test_zeros.py +34 -1
  344. scipy/signal/__init__.py +12 -23
  345. scipy/signal/_delegators.py +568 -0
  346. scipy/signal/_filter_design.py +459 -241
  347. scipy/signal/_fir_filter_design.py +262 -90
  348. scipy/signal/_lti_conversion.py +3 -2
  349. scipy/signal/_ltisys.py +118 -91
  350. scipy/signal/_max_len_seq_inner.cpython-313-aarch64-linux-musl.so +0 -0
  351. scipy/signal/_peak_finding_utils.cpython-313-aarch64-linux-musl.so +0 -0
  352. scipy/signal/_polyutils.py +172 -0
  353. scipy/signal/_short_time_fft.py +519 -70
  354. scipy/signal/_signal_api.py +30 -0
  355. scipy/signal/_signaltools.py +719 -399
  356. scipy/signal/_sigtools.cpython-313-aarch64-linux-musl.so +0 -0
  357. scipy/signal/_sosfilt.cpython-313-aarch64-linux-musl.so +0 -0
  358. scipy/signal/_spectral_py.py +230 -50
  359. scipy/signal/_spline.cpython-313-aarch64-linux-musl.so +0 -0
  360. scipy/signal/_spline_filters.py +108 -68
  361. scipy/signal/_support_alternative_backends.py +73 -0
  362. scipy/signal/_upfirdn.py +4 -1
  363. scipy/signal/_upfirdn_apply.cpython-313-aarch64-linux-musl.so +0 -0
  364. scipy/signal/_waveforms.py +2 -11
  365. scipy/signal/_wavelets.py +1 -1
  366. scipy/signal/fir_filter_design.py +1 -0
  367. scipy/signal/spline.py +4 -11
  368. scipy/signal/tests/_scipy_spectral_test_shim.py +2 -171
  369. scipy/signal/tests/test_bsplines.py +114 -79
  370. scipy/signal/tests/test_cont2discrete.py +9 -2
  371. scipy/signal/tests/test_filter_design.py +721 -481
  372. scipy/signal/tests/test_fir_filter_design.py +332 -140
  373. scipy/signal/tests/test_savitzky_golay.py +4 -3
  374. scipy/signal/tests/test_short_time_fft.py +221 -3
  375. scipy/signal/tests/test_signaltools.py +2145 -1349
  376. scipy/signal/tests/test_spectral.py +50 -6
  377. scipy/signal/tests/test_splines.py +161 -96
  378. scipy/signal/tests/test_upfirdn.py +84 -50
  379. scipy/signal/tests/test_waveforms.py +20 -0
  380. scipy/signal/tests/test_windows.py +607 -466
  381. scipy/signal/windows/_windows.py +287 -148
  382. scipy/sparse/__init__.py +23 -4
  383. scipy/sparse/_base.py +270 -108
  384. scipy/sparse/_bsr.py +7 -4
  385. scipy/sparse/_compressed.py +59 -231
  386. scipy/sparse/_construct.py +90 -38
  387. scipy/sparse/_coo.py +115 -181
  388. scipy/sparse/_csc.py +4 -4
  389. scipy/sparse/_csparsetools.cpython-313-aarch64-linux-musl.so +0 -0
  390. scipy/sparse/_csr.py +2 -2
  391. scipy/sparse/_data.py +48 -48
  392. scipy/sparse/_dia.py +105 -18
  393. scipy/sparse/_dok.py +0 -23
  394. scipy/sparse/_index.py +4 -4
  395. scipy/sparse/_matrix.py +23 -0
  396. scipy/sparse/_sparsetools.cpython-313-aarch64-linux-musl.so +0 -0
  397. scipy/sparse/_sputils.py +37 -22
  398. scipy/sparse/base.py +0 -9
  399. scipy/sparse/bsr.py +0 -14
  400. scipy/sparse/compressed.py +0 -23
  401. scipy/sparse/construct.py +0 -6
  402. scipy/sparse/coo.py +0 -14
  403. scipy/sparse/csc.py +0 -3
  404. scipy/sparse/csgraph/_flow.cpython-313-aarch64-linux-musl.so +0 -0
  405. scipy/sparse/csgraph/_matching.cpython-313-aarch64-linux-musl.so +0 -0
  406. scipy/sparse/csgraph/_min_spanning_tree.cpython-313-aarch64-linux-musl.so +0 -0
  407. scipy/sparse/csgraph/_reordering.cpython-313-aarch64-linux-musl.so +0 -0
  408. scipy/sparse/csgraph/_shortest_path.cpython-313-aarch64-linux-musl.so +0 -0
  409. scipy/sparse/csgraph/_tools.cpython-313-aarch64-linux-musl.so +0 -0
  410. scipy/sparse/csgraph/_traversal.cpython-313-aarch64-linux-musl.so +0 -0
  411. scipy/sparse/csgraph/tests/test_matching.py +14 -2
  412. scipy/sparse/csgraph/tests/test_pydata_sparse.py +4 -1
  413. scipy/sparse/csgraph/tests/test_shortest_path.py +83 -27
  414. scipy/sparse/csr.py +0 -5
  415. scipy/sparse/data.py +1 -6
  416. scipy/sparse/dia.py +0 -7
  417. scipy/sparse/dok.py +0 -10
  418. scipy/sparse/linalg/_dsolve/_superlu.cpython-313-aarch64-linux-musl.so +0 -0
  419. scipy/sparse/linalg/_dsolve/linsolve.py +9 -0
  420. scipy/sparse/linalg/_dsolve/tests/test_linsolve.py +35 -28
  421. scipy/sparse/linalg/_eigen/arpack/_arpack.cpython-313-aarch64-linux-musl.so +0 -0
  422. scipy/sparse/linalg/_eigen/arpack/arpack.py +23 -17
  423. scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py +6 -6
  424. scipy/sparse/linalg/_interface.py +17 -18
  425. scipy/sparse/linalg/_isolve/_gcrotmk.py +4 -4
  426. scipy/sparse/linalg/_isolve/iterative.py +51 -45
  427. scipy/sparse/linalg/_isolve/lgmres.py +6 -6
  428. scipy/sparse/linalg/_isolve/minres.py +5 -5
  429. scipy/sparse/linalg/_isolve/tfqmr.py +7 -7
  430. scipy/sparse/linalg/_isolve/utils.py +2 -8
  431. scipy/sparse/linalg/_matfuncs.py +1 -1
  432. scipy/sparse/linalg/_norm.py +1 -1
  433. scipy/sparse/linalg/_propack/_cpropack.cpython-313-aarch64-linux-musl.so +0 -0
  434. scipy/sparse/linalg/_propack/_dpropack.cpython-313-aarch64-linux-musl.so +0 -0
  435. scipy/sparse/linalg/_propack/_spropack.cpython-313-aarch64-linux-musl.so +0 -0
  436. scipy/sparse/linalg/_propack/_zpropack.cpython-313-aarch64-linux-musl.so +0 -0
  437. scipy/sparse/linalg/_special_sparse_arrays.py +39 -38
  438. scipy/sparse/linalg/tests/test_pydata_sparse.py +14 -0
  439. scipy/sparse/tests/test_arithmetic1d.py +5 -2
  440. scipy/sparse/tests/test_base.py +214 -42
  441. scipy/sparse/tests/test_common1d.py +7 -7
  442. scipy/sparse/tests/test_construct.py +1 -1
  443. scipy/sparse/tests/test_coo.py +272 -4
  444. scipy/sparse/tests/test_sparsetools.py +5 -0
  445. scipy/sparse/tests/test_sputils.py +36 -7
  446. scipy/spatial/_ckdtree.cpython-313-aarch64-linux-musl.so +0 -0
  447. scipy/spatial/_distance_pybind.cpython-313-aarch64-linux-musl.so +0 -0
  448. scipy/spatial/_distance_wrap.cpython-313-aarch64-linux-musl.so +0 -0
  449. scipy/spatial/_hausdorff.cpython-313-aarch64-linux-musl.so +0 -0
  450. scipy/spatial/_qhull.cpython-313-aarch64-linux-musl.so +0 -0
  451. scipy/spatial/_voronoi.cpython-313-aarch64-linux-musl.so +0 -0
  452. scipy/spatial/distance.py +49 -42
  453. scipy/spatial/tests/test_distance.py +15 -1
  454. scipy/spatial/tests/test_kdtree.py +1 -0
  455. scipy/spatial/tests/test_qhull.py +7 -2
  456. scipy/spatial/transform/__init__.py +5 -3
  457. scipy/spatial/transform/_rigid_transform.cpython-313-aarch64-linux-musl.so +0 -0
  458. scipy/spatial/transform/_rotation.cpython-313-aarch64-linux-musl.so +0 -0
  459. scipy/spatial/transform/tests/test_rigid_transform.py +1221 -0
  460. scipy/spatial/transform/tests/test_rotation.py +1213 -832
  461. scipy/spatial/transform/tests/test_rotation_groups.py +3 -3
  462. scipy/spatial/transform/tests/test_rotation_spline.py +29 -8
  463. scipy/special/__init__.py +1 -47
  464. scipy/special/_add_newdocs.py +34 -772
  465. scipy/special/_basic.py +22 -25
  466. scipy/special/_comb.cpython-313-aarch64-linux-musl.so +0 -0
  467. scipy/special/_ellip_harm_2.cpython-313-aarch64-linux-musl.so +0 -0
  468. scipy/special/_gufuncs.cpython-313-aarch64-linux-musl.so +0 -0
  469. scipy/special/_logsumexp.py +67 -58
  470. scipy/special/_orthogonal.pyi +1 -1
  471. scipy/special/_specfun.cpython-313-aarch64-linux-musl.so +0 -0
  472. scipy/special/_special_ufuncs.cpython-313-aarch64-linux-musl.so +0 -0
  473. scipy/special/_spherical_bessel.py +4 -4
  474. scipy/special/_support_alternative_backends.py +212 -119
  475. scipy/special/_test_internal.cpython-313-aarch64-linux-musl.so +0 -0
  476. scipy/special/_testutils.py +4 -4
  477. scipy/special/_ufuncs.cpython-313-aarch64-linux-musl.so +0 -0
  478. scipy/special/_ufuncs.pyi +1 -0
  479. scipy/special/_ufuncs.pyx +215 -1400
  480. scipy/special/_ufuncs_cxx.cpython-313-aarch64-linux-musl.so +0 -0
  481. scipy/special/_ufuncs_cxx.pxd +2 -15
  482. scipy/special/_ufuncs_cxx.pyx +5 -44
  483. scipy/special/_ufuncs_cxx_defs.h +2 -16
  484. scipy/special/_ufuncs_defs.h +0 -8
  485. scipy/special/cython_special.cpython-313-aarch64-linux-musl.so +0 -0
  486. scipy/special/cython_special.pxd +1 -1
  487. scipy/special/tests/_cython_examples/meson.build +10 -1
  488. scipy/special/tests/test_basic.py +153 -20
  489. scipy/special/tests/test_boost_ufuncs.py +3 -0
  490. scipy/special/tests/test_cdflib.py +35 -11
  491. scipy/special/tests/test_gammainc.py +16 -0
  492. scipy/special/tests/test_hyp2f1.py +2 -2
  493. scipy/special/tests/test_log1mexp.py +85 -0
  494. scipy/special/tests/test_logsumexp.py +206 -64
  495. scipy/special/tests/test_mpmath.py +1 -0
  496. scipy/special/tests/test_nan_inputs.py +1 -1
  497. scipy/special/tests/test_orthogonal.py +17 -18
  498. scipy/special/tests/test_sf_error.py +3 -2
  499. scipy/special/tests/test_sph_harm.py +6 -7
  500. scipy/special/tests/test_support_alternative_backends.py +211 -76
  501. scipy/stats/__init__.py +4 -1
  502. scipy/stats/_ansari_swilk_statistics.cpython-313-aarch64-linux-musl.so +0 -0
  503. scipy/stats/_axis_nan_policy.py +5 -12
  504. scipy/stats/_biasedurn.cpython-313-aarch64-linux-musl.so +0 -0
  505. scipy/stats/_continued_fraction.py +387 -0
  506. scipy/stats/_continuous_distns.py +277 -310
  507. scipy/stats/_correlation.py +1 -1
  508. scipy/stats/_covariance.py +6 -3
  509. scipy/stats/_discrete_distns.py +39 -32
  510. scipy/stats/_distn_infrastructure.py +39 -12
  511. scipy/stats/_distribution_infrastructure.py +920 -238
  512. scipy/stats/_entropy.py +9 -10
  513. scipy/{_lib → stats}/_finite_differences.py +1 -1
  514. scipy/stats/_hypotests.py +83 -50
  515. scipy/stats/_kde.py +53 -49
  516. scipy/stats/_ksstats.py +1 -1
  517. scipy/stats/_levy_stable/__init__.py +7 -15
  518. scipy/stats/_levy_stable/levyst.cpython-313-aarch64-linux-musl.so +0 -0
  519. scipy/stats/_morestats.py +118 -73
  520. scipy/stats/_mstats_basic.py +13 -17
  521. scipy/stats/_mstats_extras.py +8 -8
  522. scipy/stats/_multivariate.py +89 -113
  523. scipy/stats/_new_distributions.py +97 -20
  524. scipy/stats/_page_trend_test.py +12 -5
  525. scipy/stats/_probability_distribution.py +265 -43
  526. scipy/stats/_qmc.py +14 -9
  527. scipy/stats/_qmc_cy.cpython-313-aarch64-linux-musl.so +0 -0
  528. scipy/stats/_qmvnt.py +16 -95
  529. scipy/stats/_qmvnt_cy.cpython-313-aarch64-linux-musl.so +0 -0
  530. scipy/stats/_quantile.py +335 -0
  531. scipy/stats/_rcont/rcont.cpython-313-aarch64-linux-musl.so +0 -0
  532. scipy/stats/_resampling.py +5 -30
  533. scipy/stats/_sampling.py +1 -1
  534. scipy/stats/_sobol.cpython-313-aarch64-linux-musl.so +0 -0
  535. scipy/stats/_stats.cpython-313-aarch64-linux-musl.so +0 -0
  536. scipy/stats/_stats_mstats_common.py +21 -2
  537. scipy/stats/_stats_py.py +551 -477
  538. scipy/stats/_stats_pythran.cpython-313-aarch64-linux-musl.so +0 -0
  539. scipy/stats/_unuran/unuran_wrapper.cpython-313-aarch64-linux-musl.so +0 -0
  540. scipy/stats/_unuran/unuran_wrapper.pyi +2 -1
  541. scipy/stats/_variation.py +6 -8
  542. scipy/stats/_wilcoxon.py +13 -7
  543. scipy/stats/tests/common_tests.py +6 -4
  544. scipy/stats/tests/test_axis_nan_policy.py +62 -24
  545. scipy/stats/tests/test_continued_fraction.py +173 -0
  546. scipy/stats/tests/test_continuous.py +379 -60
  547. scipy/stats/tests/test_continuous_basic.py +18 -12
  548. scipy/stats/tests/test_discrete_basic.py +14 -8
  549. scipy/stats/tests/test_discrete_distns.py +16 -16
  550. scipy/stats/tests/test_distributions.py +95 -75
  551. scipy/stats/tests/test_entropy.py +40 -48
  552. scipy/stats/tests/test_fit.py +4 -3
  553. scipy/stats/tests/test_hypotests.py +153 -24
  554. scipy/stats/tests/test_kdeoth.py +109 -41
  555. scipy/stats/tests/test_marray.py +289 -0
  556. scipy/stats/tests/test_morestats.py +81 -49
  557. scipy/stats/tests/test_mstats_basic.py +3 -3
  558. scipy/stats/tests/test_multivariate.py +434 -83
  559. scipy/stats/tests/test_qmc.py +13 -10
  560. scipy/stats/tests/test_quantile.py +199 -0
  561. scipy/stats/tests/test_rank.py +119 -112
  562. scipy/stats/tests/test_resampling.py +47 -56
  563. scipy/stats/tests/test_sampling.py +9 -4
  564. scipy/stats/tests/test_stats.py +799 -939
  565. scipy/stats/tests/test_variation.py +8 -6
  566. scipy/version.py +2 -2
  567. {scipy-1.15.3.dist-info → scipy-1.16.0.dist-info}/LICENSE.txt +4 -4
  568. {scipy-1.15.3.dist-info → scipy-1.16.0.dist-info}/METADATA +11 -11
  569. {scipy-1.15.3.dist-info → scipy-1.16.0.dist-info}/RECORD +1284 -1291
  570. scipy.libs/libgcc_s-2d945d6c.so.1 +0 -0
  571. scipy.libs/libgfortran-67378ab2.so.5.0.0 +0 -0
  572. scipy.libs/{libstdc++-1b614e01.so.6.0.32 → libstdc++-85f2cd6d.so.6.0.33} +0 -0
  573. scipy/_lib/array_api_extra/_funcs.py +0 -484
  574. scipy/_lib/array_api_extra/_typing.py +0 -8
  575. scipy/interpolate/_bspl.cpython-313-aarch64-linux-musl.so +0 -0
  576. scipy/optimize/_cobyla.cpython-313-aarch64-linux-musl.so +0 -0
  577. scipy/optimize/_cython_nnls.cpython-313-aarch64-linux-musl.so +0 -0
  578. scipy/optimize/_slsqp.cpython-313-aarch64-linux-musl.so +0 -0
  579. scipy/spatial/qhull_src/COPYING.txt +0 -38
  580. scipy/special/libsf_error_state.so +0 -0
  581. scipy/special/tests/test_log_softmax.py +0 -109
  582. scipy/special/tests/test_xsf_cuda.py +0 -114
  583. scipy/special/xsf/binom.h +0 -89
  584. scipy/special/xsf/cdflib.h +0 -100
  585. scipy/special/xsf/cephes/airy.h +0 -307
  586. scipy/special/xsf/cephes/besselpoly.h +0 -51
  587. scipy/special/xsf/cephes/beta.h +0 -257
  588. scipy/special/xsf/cephes/cbrt.h +0 -131
  589. scipy/special/xsf/cephes/chbevl.h +0 -85
  590. scipy/special/xsf/cephes/chdtr.h +0 -193
  591. scipy/special/xsf/cephes/const.h +0 -87
  592. scipy/special/xsf/cephes/ellie.h +0 -293
  593. scipy/special/xsf/cephes/ellik.h +0 -251
  594. scipy/special/xsf/cephes/ellpe.h +0 -107
  595. scipy/special/xsf/cephes/ellpk.h +0 -117
  596. scipy/special/xsf/cephes/expn.h +0 -260
  597. scipy/special/xsf/cephes/gamma.h +0 -398
  598. scipy/special/xsf/cephes/hyp2f1.h +0 -596
  599. scipy/special/xsf/cephes/hyperg.h +0 -361
  600. scipy/special/xsf/cephes/i0.h +0 -149
  601. scipy/special/xsf/cephes/i1.h +0 -158
  602. scipy/special/xsf/cephes/igam.h +0 -421
  603. scipy/special/xsf/cephes/igam_asymp_coeff.h +0 -195
  604. scipy/special/xsf/cephes/igami.h +0 -313
  605. scipy/special/xsf/cephes/j0.h +0 -225
  606. scipy/special/xsf/cephes/j1.h +0 -198
  607. scipy/special/xsf/cephes/jv.h +0 -715
  608. scipy/special/xsf/cephes/k0.h +0 -164
  609. scipy/special/xsf/cephes/k1.h +0 -163
  610. scipy/special/xsf/cephes/kn.h +0 -243
  611. scipy/special/xsf/cephes/lanczos.h +0 -112
  612. scipy/special/xsf/cephes/ndtr.h +0 -275
  613. scipy/special/xsf/cephes/poch.h +0 -85
  614. scipy/special/xsf/cephes/polevl.h +0 -167
  615. scipy/special/xsf/cephes/psi.h +0 -194
  616. scipy/special/xsf/cephes/rgamma.h +0 -111
  617. scipy/special/xsf/cephes/scipy_iv.h +0 -811
  618. scipy/special/xsf/cephes/shichi.h +0 -248
  619. scipy/special/xsf/cephes/sici.h +0 -224
  620. scipy/special/xsf/cephes/sindg.h +0 -221
  621. scipy/special/xsf/cephes/tandg.h +0 -139
  622. scipy/special/xsf/cephes/trig.h +0 -58
  623. scipy/special/xsf/cephes/unity.h +0 -186
  624. scipy/special/xsf/cephes/zeta.h +0 -172
  625. scipy/special/xsf/config.h +0 -304
  626. scipy/special/xsf/digamma.h +0 -205
  627. scipy/special/xsf/error.h +0 -57
  628. scipy/special/xsf/evalpoly.h +0 -47
  629. scipy/special/xsf/expint.h +0 -266
  630. scipy/special/xsf/hyp2f1.h +0 -694
  631. scipy/special/xsf/iv_ratio.h +0 -173
  632. scipy/special/xsf/lambertw.h +0 -150
  633. scipy/special/xsf/loggamma.h +0 -163
  634. scipy/special/xsf/sici.h +0 -200
  635. scipy/special/xsf/tools.h +0 -427
  636. scipy/special/xsf/trig.h +0 -164
  637. scipy/special/xsf/wright_bessel.h +0 -843
  638. scipy/special/xsf/zlog1.h +0 -35
  639. scipy/stats/_mvn.cpython-313-aarch64-linux-musl.so +0 -0
  640. scipy.libs/libgcc_s-7393e603.so.1 +0 -0
  641. scipy.libs/libgfortran-eb933d8e.so.5.0.0 +0 -0
  642. {scipy-1.15.3.dist-info → scipy-1.16.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,1221 @@
1
+ import pytest
2
+
3
+ import numpy as np
4
+ from scipy.spatial.transform import Rotation, RigidTransform
5
+ from scipy.spatial.transform._rigid_transform import normalize_dual_quaternion
6
+ from scipy._lib._array_api import (
7
+ is_lazy_array,
8
+ xp_vector_norm,
9
+ is_numpy,
10
+ xp_assert_close,
11
+ )
12
+ import scipy._lib.array_api_extra as xpx
13
+
14
+
15
+ pytestmark = pytest.mark.skip_xp_backends(np_only=True)
16
+
17
+
18
+ def rotation_to_xp(r: Rotation, xp):
19
+ return Rotation.from_quat(xp.asarray(r.as_quat()))
20
+
21
+
22
+ def rigid_transform_to_xp(r: RigidTransform, xp):
23
+ return RigidTransform.from_matrix(xp.asarray(r.as_matrix()))
24
+
25
+
26
+ def test_repr(xp):
27
+ actual = repr(RigidTransform.from_matrix(xp.eye(4)))
28
+ expected = """\
29
+ RigidTransform.from_matrix(array([[1., 0., 0., 0.],
30
+ [0., 1., 0., 0.],
31
+ [0., 0., 1., 0.],
32
+ [0., 0., 0., 1.]]))"""
33
+ assert actual == expected
34
+
35
+ tf = RigidTransform.from_matrix(xp.asarray(RigidTransform.identity(2).as_matrix()))
36
+ actual = repr(tf)
37
+ expected = """\
38
+ RigidTransform.from_matrix(array([[[1., 0., 0., 0.],
39
+ [0., 1., 0., 0.],
40
+ [0., 0., 1., 0.],
41
+ [0., 0., 0., 1.]],
42
+
43
+ [[1., 0., 0., 0.],
44
+ [0., 1., 0., 0.],
45
+ [0., 0., 1., 0.],
46
+ [0., 0., 0., 1.]]]))"""
47
+ assert actual == expected
48
+
49
+
50
+ def test_from_rotation(xp):
51
+ atol = 1e-12
52
+
53
+ # Test single rotation
54
+ r = Rotation.from_matrix(xp.eye(3))
55
+ tf = RigidTransform.from_rotation(r)
56
+ xp_assert_close(tf.as_matrix(), xp.eye(4), atol=atol)
57
+ assert tf.single
58
+
59
+ r = Rotation.from_euler('z', xp.asarray(90), degrees=True)
60
+ tf = RigidTransform.from_rotation(r)
61
+ xp_assert_close(tf.as_matrix()[:3, :3], r.as_matrix(), atol=atol)
62
+ xp_assert_close(tf.as_matrix()[:3, 3], xp.asarray([0.0, 0, 0]), atol=atol)
63
+ xp_assert_close(tf.as_matrix()[3, :], xp.asarray([0.0, 0, 0, 1]), atol=atol)
64
+ assert tf.single
65
+
66
+ # Test multiple rotations
67
+ r = Rotation.from_euler('zyx', xp.asarray([[90, 0, 0], [0, 90, 0]]), degrees=True)
68
+ tf = RigidTransform.from_rotation(r)
69
+ xp_assert_close(tf.as_matrix()[:, :3, :3], r.as_matrix(), atol=atol)
70
+ xp_assert_close(tf.as_matrix()[:, :3, 3], xp.asarray([[0.0, 0, 0], [0, 0, 0]]),
71
+ atol=atol)
72
+ xp_assert_close(tf.as_matrix()[:, 3, :], xp.asarray([[0.0, 0, 0, 1], [0, 0, 0, 1]]),
73
+ atol=atol)
74
+ assert not tf.single
75
+
76
+
77
+ def test_from_translation(xp):
78
+ # Test single translation
79
+ t = xp.asarray([1, 2, 3])
80
+ tf = RigidTransform.from_translation(t)
81
+ expected = xp.eye(4)
82
+ expected = xpx.at(expected)[..., :3, 3].set(t)
83
+ xp_assert_close(tf.as_matrix(), expected)
84
+ assert tf.single
85
+
86
+ # Test multiple translations
87
+ t = xp.asarray([[1, 2, 3], [4, 5, 6]])
88
+ tf = RigidTransform.from_translation(t)
89
+ for i in range(t.shape[0]):
90
+ expected = xp.eye(4)
91
+ expected = xpx.at(expected)[..., :3, 3].set(t[i, ...])
92
+ xp_assert_close(tf.as_matrix()[i, ...], expected)
93
+ assert not tf.single
94
+
95
+
96
+ def test_from_translation_array_like():
97
+ # Test single translation
98
+ t = [1, 2, 3]
99
+ tf = RigidTransform.from_translation(t)
100
+ tf_expected = RigidTransform.from_translation(np.array(t))
101
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix())
102
+ assert tf.single
103
+
104
+ # Test multiple translations
105
+ t = [[1, 2, 3], [4, 5, 6]]
106
+ tf = RigidTransform.from_translation(t)
107
+ tf_expected = RigidTransform.from_translation(np.array(t))
108
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix())
109
+ assert not tf.single
110
+
111
+
112
+ def test_from_matrix(xp):
113
+ atol = 1e-12
114
+
115
+ # Test single transform matrix
116
+ matrix = xp.eye(4)
117
+ matrix = xpx.at(matrix)[..., :3, 3].set(xp.asarray([1, 2, 3]))
118
+ tf = RigidTransform.from_matrix(matrix)
119
+ xp_assert_close(tf.as_matrix(), matrix, atol=atol)
120
+ assert tf.single
121
+
122
+ # Test multiple transform matrices
123
+ matrices = xp.repeat(xp.eye(4)[None, ...], 2, axis=0)
124
+ matrices = xpx.at(matrices)[0, :3, 3].set(xp.asarray([1, 2, 3]))
125
+ matrices = xpx.at(matrices)[1, :3, 3].set(xp.asarray([4, 5, 6]))
126
+ tf = RigidTransform.from_matrix(matrices)
127
+ xp_assert_close(tf.as_matrix(), matrices, atol=atol)
128
+ assert not tf.single
129
+
130
+ # Test non-1 determinant
131
+ matrix = xp.eye(4)
132
+ matrix = xpx.at(matrix)[..., :3, :3].set(xp.eye(3) * 2.0)
133
+ tf = RigidTransform.from_matrix(matrix)
134
+ xp_assert_close(tf.as_matrix(), xp.eye(4), atol=atol)
135
+
136
+ # Test non-orthogonal rotation matrix
137
+ matrix = xp.asarray([[1, 1, 0, 0],
138
+ [0, 1, 0, 0],
139
+ [0, 0, 1, 0],
140
+ [0, 0, 0, 1]])
141
+ tf = RigidTransform.from_matrix(matrix)
142
+ expected = xp.asarray([[0.894427, 0.447214, 0, 0],
143
+ [-0.447214, 0.894427, 0, 0],
144
+ [0, 0, 1, 0],
145
+ [0, 0, 0, 1]])
146
+ xp_assert_close(tf.as_matrix(), expected, atol=1e-6)
147
+
148
+ # Test invalid matrix
149
+ invalid = xp.eye(4)
150
+ invalid = xpx.at(invalid)[..., 3, 3].set(2) # Invalid last row
151
+ if is_lazy_array(invalid):
152
+ tf = RigidTransform.from_matrix(invalid)
153
+ assert xp.all(xp.isnan(tf.as_matrix()))
154
+ else:
155
+ with pytest.raises(ValueError):
156
+ RigidTransform.from_matrix(invalid)
157
+
158
+
159
+ def test_from_matrix_array_like():
160
+ # Test single transform matrix
161
+ matrix = [[1, 0, 0, 0],
162
+ [0, 1, 0, 0],
163
+ [0, 0, 1, 0],
164
+ [0, 0, 0, 1]]
165
+ expected = np.eye(4)
166
+ tf = RigidTransform.from_matrix(matrix)
167
+ xp_assert_close(tf.as_matrix(), expected)
168
+ assert tf.single
169
+
170
+ # Test multiple transform matrices
171
+ matrices = [matrix, matrix]
172
+ tf = RigidTransform.from_matrix(matrices)
173
+ for i in range(len(matrices)):
174
+ xp_assert_close(tf.as_matrix()[i, ...], expected)
175
+ assert not tf.single
176
+
177
+
178
+ def test_from_components(xp):
179
+ atol = 1e-12
180
+
181
+ # Test single rotation and translation
182
+ t = xp.asarray([1, 2, 3])
183
+ r = Rotation.from_euler("zyx", xp.asarray([90, 0, 0]), degrees=True)
184
+ tf = RigidTransform.from_components(t, r)
185
+
186
+ expected = xp.zeros((4, 4))
187
+ expected = xpx.at(expected)[..., :3, :3].set(r.as_matrix())
188
+ expected = xpx.at(expected)[..., :3, 3].set(t)
189
+ expected = xpx.at(expected)[..., 3, 3].set(1)
190
+ xp_assert_close(tf.as_matrix(), expected, atol=atol)
191
+ assert tf.single
192
+
193
+ # Test single rotation and multiple translations
194
+ t = xp.asarray([[1, 2, 3], [4, 5, 6]])
195
+ r = Rotation.from_euler('z', xp.asarray(90), degrees=True)
196
+ tf = RigidTransform.from_components(t, r)
197
+ assert not tf.single
198
+
199
+ for i in range(t.shape[0]):
200
+ expected = xp.zeros((4, 4))
201
+ expected = xpx.at(expected)[..., :3, :3].set(r.as_matrix())
202
+ expected = xpx.at(expected)[..., :3, 3].set(t[i, ...])
203
+ expected = xpx.at(expected)[..., 3, 3].set(1)
204
+ xp_assert_close(tf.as_matrix()[i, ...], expected, atol=atol)
205
+
206
+ # Test multiple rotations and translations
207
+ t = xp.asarray([[1, 2, 3], [4, 5, 6]])
208
+ r = Rotation.from_euler('zyx', xp.asarray([[90, 0, 0], [0, 90, 0]]), degrees=True)
209
+ tf = RigidTransform.from_components(t, r)
210
+ assert not tf.single
211
+
212
+ for i in range(t.shape[0]):
213
+ expected = xp.zeros((4, 4))
214
+ expected = xpx.at(expected)[..., :3, :3].set(r.as_matrix()[i, ...])
215
+ expected = xpx.at(expected)[..., :3, 3].set(t[i, ...])
216
+ expected = xpx.at(expected)[..., 3, 3].set(1)
217
+ xp_assert_close(tf.as_matrix()[i, ...], expected, atol=atol)
218
+
219
+
220
+ def test_from_components_array_like():
221
+ rng = np.random.default_rng(123)
222
+ # Test single rotation and translation
223
+ t = [1, 2, 3]
224
+ r = Rotation.random(rng=rng)
225
+ tf = RigidTransform.from_components(t, r)
226
+ tf_expected = RigidTransform.from_components(np.array(t), r)
227
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12)
228
+ assert tf.single
229
+
230
+ # Test multiple rotations and translations
231
+ t = [[1, 2, 3], [4, 5, 6]]
232
+ r = Rotation.random(len(t), rng=rng)
233
+ tf = RigidTransform.from_components(t, r)
234
+ tf_expected = RigidTransform.from_components(np.array(t), r)
235
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12)
236
+ assert not tf.single
237
+
238
+
239
+
240
+ def test_as_components(xp):
241
+ atol = 1e-12
242
+ n = 10
243
+ rng = np.random.default_rng(123)
244
+ t = xp.asarray(rng.normal(size=(n, 3)))
245
+ r = rotation_to_xp(Rotation.random(n, rng=rng), xp=xp)
246
+ tf = RigidTransform.from_components(t, r)
247
+ new_t, new_r = tf.as_components()
248
+ assert all(new_r.approx_equal(r, atol=atol))
249
+ xp_assert_close(new_t, t, atol=atol)
250
+
251
+
252
+ def test_from_exp_coords(xp):
253
+ # example from 3.3 of
254
+ # https://hades.mech.northwestern.edu/images/2/25/MR-v2.pdf
255
+ angle1 = np.deg2rad(30.0)
256
+ mat = xp.asarray([
257
+ [np.cos(angle1), -np.sin(angle1), 0.0, 1.0],
258
+ [np.sin(angle1), np.cos(angle1), 0.0, 2.0],
259
+ [0.0, 0.0, 1.0, 0.0],
260
+ [0.0, 0.0, 0.0, 1.0]
261
+ ])
262
+ tf1 = RigidTransform.from_matrix(mat)
263
+ angle2 = np.deg2rad(60.0)
264
+ mat = xp.asarray([
265
+ [np.cos(angle2), -np.sin(angle2), 0.0, 2.0],
266
+ [np.sin(angle2), np.cos(angle2), 0.0, 1.0],
267
+ [0.0, 0.0, 1.0, 0.0],
268
+ [0.0, 0.0, 0.0, 1.0]
269
+ ])
270
+ tf2 = RigidTransform.from_matrix(mat)
271
+ expected = tf2 * tf1.inv()
272
+ actual = RigidTransform.from_exp_coords(
273
+ np.deg2rad(30.0) * xp.asarray([0.0, 0.0, 1.0, 3.37, -3.37, 0.0]))
274
+ xp_assert_close(actual.as_matrix(), expected.as_matrix(), atol=1e-2)
275
+
276
+ # test cases generated by comparison to pytransform3d
277
+ exp_coords = xp.asarray([
278
+ [-2.01041204, -0.52983629, 0.65773501,
279
+ 0.10386614, 0.05855009, 0.54959179],
280
+ [-0.22537438, -0.24132627, -2.4747121,
281
+ -0.09158594, 1.88075832, -0.03197204]
282
+ ])
283
+ expected_matrix = xp.asarray([
284
+ [[0.76406621, 0.10504613, -0.63652819, -0.10209961],
285
+ [0.59956454, -0.47987325, 0.64050295, 0.40158789],
286
+ [-0.2381705, -0.87102639, -0.42963687, 0.19637636],
287
+ [0., 0., 0., 1.]],
288
+ [[-0.78446989, 0.61157488, 0.10287448, 1.33330055],
289
+ [-0.58017785, -0.78232107, 0.22664378, 0.52660831],
290
+ [0.21909052, 0.11810973, 0.96852952, -0.02968529],
291
+ [0., 0., 0., 1.]]
292
+ ])
293
+ xp_assert_close(
294
+ RigidTransform.from_exp_coords(exp_coords).as_matrix(),
295
+ expected_matrix, atol=1e-8)
296
+
297
+ # identity
298
+ xp_assert_close(
299
+ RigidTransform.from_exp_coords(xp.zeros(6)).as_matrix(),
300
+ xp.eye(4), atol=1e-12)
301
+
302
+ # only translation
303
+ expected_matrix = xp.asarray([
304
+ [[1.0, 0.0, 0.0, 3.0],
305
+ [0.0, 1.0, 0.0, -5.4],
306
+ [0.0, 0.0, 1.0, 100.2],
307
+ [0.0, 0.0, 0.0, 1.0]],
308
+ [[1.0, 0.0, 0.0, -3.0],
309
+ [0.0, 1.0, 0.0, 13.3],
310
+ [0.0, 0.0, 1.0, 1.3],
311
+ [0.0, 0.0, 0.0, 1.0]]
312
+ ])
313
+ actual = RigidTransform.from_exp_coords(xp.asarray([
314
+ [0.0, 0.0, 0.0, 3.0, -5.4, 100.2],
315
+ [0.0, 0.0, 0.0, -3.0, 13.3, 1.3],
316
+ ]))
317
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=1e-12)
318
+
319
+ # only rotation
320
+ rot = Rotation.from_euler(
321
+ 'zyx',
322
+ xp.asarray([[34, -12, 0.5],
323
+ [-102, -55, 30]]),
324
+ degrees=True)
325
+ rotvec = rot.as_rotvec()
326
+ expected_matrix = xp.repeat(xp.eye(4)[None, ...], 2, axis=0)
327
+ expected_matrix = xpx.at(expected_matrix)[..., :3, :3].set(rot.as_matrix())
328
+ actual = RigidTransform.from_exp_coords(
329
+ xp.concat((rotvec, xp.zeros((2, 3))), axis=-1))
330
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=1e-12)
331
+
332
+
333
+ def test_from_exp_coords_array_like():
334
+ rng = np.random.default_rng(123)
335
+ # Test single transform
336
+ t = np.array([1, 2, 3])
337
+ r = Rotation.random(rng=rng)
338
+ tf_expected = RigidTransform.from_components(t, r)
339
+ exp_coords = tf_expected.as_exp_coords().tolist()
340
+ assert isinstance(exp_coords, list)
341
+ tf = RigidTransform.from_exp_coords(exp_coords)
342
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12)
343
+
344
+ # Test multiple transforms
345
+ t = [[1, 2, 3], [4, 5, 6]]
346
+ r = Rotation.random(len(t), rng=rng)
347
+ tf_expected = RigidTransform.from_components(t, r)
348
+ exp_coords = tf_expected.as_exp_coords().tolist()
349
+ assert isinstance(exp_coords, list)
350
+ tf = RigidTransform.from_exp_coords(exp_coords)
351
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12)
352
+
353
+
354
+ def test_as_exp_coords(xp):
355
+ # identity
356
+ expected = xp.zeros(6)
357
+ actual = RigidTransform.from_exp_coords(expected).as_exp_coords()
358
+ xp_assert_close(actual, expected, atol=1e-12)
359
+
360
+ rng = np.random.default_rng(10)
361
+
362
+ # pure rotation
363
+ rot_vec = xp.asarray(rng.normal(scale=0.1, size=(1000, 3)))
364
+ tf = RigidTransform.from_rotation(Rotation.from_rotvec(rot_vec))
365
+ exp_coords = tf.as_exp_coords()
366
+ xp_assert_close(exp_coords[:, :3], rot_vec, rtol=1e-13)
367
+ expected = xp.zeros_like(rot_vec)
368
+ xp_assert_close(exp_coords[:, 3:], expected, atol=1e-16)
369
+
370
+ # pure translation
371
+ translation = xp.asarray(rng.normal(scale=100.0, size=(1000, 3)))
372
+ tf = RigidTransform.from_translation(translation)
373
+ exp_coords = tf.as_exp_coords()
374
+ xp_assert_close(exp_coords[:, :3], expected, atol=1e-16)
375
+ xp_assert_close(exp_coords[:, 3:], translation, rtol=1e-15)
376
+
377
+
378
+ def test_from_dual_quat(xp):
379
+ # identity
380
+ xp_assert_close(
381
+ RigidTransform.from_dual_quat(
382
+ xp.asarray([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0])).as_matrix(),
383
+ xp.eye(4), atol=1e-12)
384
+ xp_assert_close(
385
+ RigidTransform.from_dual_quat(
386
+ xp.asarray([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
387
+ scalar_first=True).as_matrix(),
388
+ xp.eye(4), atol=1e-12)
389
+
390
+ # only translation
391
+ actual = RigidTransform.from_dual_quat(
392
+ xp.asarray([0, 0, 0, 1, 0.25, 0.15, -0.7, 0]))
393
+ expected_matrix = xp.asarray([
394
+ [1, 0, 0, 0.5],
395
+ [0, 1, 0, 0.3],
396
+ [0, 0, 1, -1.4],
397
+ [0, 0, 0, 1]
398
+ ])
399
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=1e-12)
400
+ actual = RigidTransform.from_dual_quat(
401
+ xp.asarray([1, 0, 0, 0, 0, 0.25, 0.15, -0.7]), scalar_first=True)
402
+ expected_matrix = xp.asarray([
403
+ [1, 0, 0, 0.5],
404
+ [0, 1, 0, 0.3],
405
+ [0, 0, 1, -1.4],
406
+ [0, 0, 0, 1]
407
+ ])
408
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=1e-12)
409
+
410
+ # only rotation
411
+ actual_rot = Rotation.from_euler("xyz", xp.asarray([65, -13, 90]), degrees=True)
412
+ actual = RigidTransform.from_dual_quat(
413
+ xp.concat((actual_rot.as_quat(), xp.zeros(4)), axis=-1))
414
+ expected_matrix = xp.eye(4)
415
+ expected_matrix = xpx.at(expected_matrix)[..., :3, :3].set(actual_rot.as_matrix())
416
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=1e-12)
417
+
418
+ actual = RigidTransform.from_dual_quat(
419
+ xp.concat((actual_rot.as_quat(scalar_first=True), xp.zeros(4)), axis=-1),
420
+ scalar_first=True)
421
+ expected_matrix = xp.eye(4)
422
+ expected_matrix = xpx.at(expected_matrix)[..., :3, :3].set(actual_rot.as_matrix())
423
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=1e-12)
424
+
425
+ # rotation and translation
426
+ # rtol is set to 1e-7 because xp_assert_close deviates from
427
+ # np.testing.assert_allclose in that it does not automatically default to 1e-7 for
428
+ # floating point inputs.
429
+ # See https://numpy.org/doc/2.2/reference/generated/numpy.testing.assert_allclose.html
430
+ actual = RigidTransform.from_dual_quat(
431
+ xp.asarray(
432
+ [[0.0617101, -0.06483886, 0.31432811, 0.94508498,
433
+ 0.04985168, -0.26119618, 0.1691491, -0.07743254],
434
+ [0.19507259, 0.49404931, -0.06091285, 0.8450749,
435
+ 0.65049656, -0.30782513, 0.16566752, 0.04174544]]))
436
+ expected_matrix = xp.asarray(
437
+ [[[0.79398752, -0.60213598, -0.08376202, 0.24605262],
438
+ [0.58613113, 0.79477941, -0.15740392, -0.4932833],
439
+ [0.16135089, 0.07588122, 0.98397557, 0.34262676],
440
+ [0., 0., 0., 1.]],
441
+ [[0.50440981, 0.2957028, 0.81125249, 1.20934468],
442
+ [0.08979911, 0.91647262, -0.3898898, -0.70540077],
443
+ [-0.8587822, 0.26951399, 0.43572393, -0.47776265],
444
+ [0., 0., 0., 1.]]])
445
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=1e-12, rtol=1e-7)
446
+
447
+ actual = RigidTransform.from_dual_quat(
448
+ xp.asarray(
449
+ [[0.94508498, 0.0617101, -0.06483886, 0.31432811,
450
+ -0.07743254, 0.04985168, -0.26119618, 0.1691491],
451
+ [0.8450749, 0.19507259, 0.49404931, -0.06091285,
452
+ 0.04174544, 0.65049656, -0.30782513, 0.16566752]]),
453
+ scalar_first=True)
454
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=1e-12, rtol=1e-7)
455
+
456
+ # unnormalized dual quaternions
457
+
458
+ # invalid real quaternion with norm 0
459
+ actual = RigidTransform.from_dual_quat(xp.zeros(8))
460
+ xp_assert_close(actual.as_matrix(), xp.eye(4), atol=1e-12)
461
+
462
+ # real quaternion with norm != 1
463
+ unnormalized_dual_quat = xp.asarray(
464
+ [-0.2547655, 1.23506123, 0.20230088, 0.24247194, # norm 1.3
465
+ 0.38559628, 0.08184063, 0.1755943, -0.1582222] # orthogonal
466
+ )
467
+ xp_assert_close(xp_vector_norm(unnormalized_dual_quat[:4]), xp.asarray(1.3)[()],
468
+ atol=1e-12)
469
+ xp_assert_close(xp.vecdot(unnormalized_dual_quat[:4],
470
+ unnormalized_dual_quat[4:])[()],
471
+ xp.asarray(0.0)[()], atol=1e-8)
472
+
473
+ dual_quat = RigidTransform.from_dual_quat(
474
+ unnormalized_dual_quat).as_dual_quat()
475
+ xp_assert_close(xp_vector_norm(dual_quat[:4]), xp.asarray(1.0)[()], atol=1e-12)
476
+ xp_assert_close(xp.vecdot(dual_quat[:4], dual_quat[4:])[()], xp.asarray(0.0)[()],
477
+ atol=1e-12)
478
+
479
+ # real and dual quaternion are not orthogonal
480
+ unnormalized_dual_quat = xp.asarray(
481
+ [0.20824458, 0.75098079, 0.54542913, -0.30849493, # unit norm
482
+ -0.16051025, 0.10742978, 0.21277201, 0.20596935] # not orthogonal
483
+ )
484
+ xp_assert_close(xp_vector_norm(unnormalized_dual_quat[:4]), xp.asarray(1.0)[()],
485
+ atol=1e-12)
486
+ assert xp.vecdot(unnormalized_dual_quat[:4], unnormalized_dual_quat[4:]) != 0.0
487
+ dual_quat = RigidTransform.from_dual_quat(
488
+ unnormalized_dual_quat).as_dual_quat()
489
+ xp_assert_close(xp_vector_norm(dual_quat[:4]), xp.asarray(1.0)[()], atol=1e-12)
490
+ xp_assert_close(xp.vecdot(dual_quat[:4], dual_quat[4:])[()], xp.asarray(0.0)[()],
491
+ atol=1e-12)
492
+
493
+ # invalid real quaternion with norm 0, non-orthogonal dual quaternion
494
+ unnormalized_dual_quat = xp.asarray(
495
+ [0.0, 0.0, 0.0, 0.0, -0.16051025, 0.10742978, 0.21277201, 0.20596935])
496
+ assert xp.vecdot(xp.asarray([0.0, 0, 0, 1]), unnormalized_dual_quat[4:]) != 0.0
497
+ dual_quat = RigidTransform.from_dual_quat(
498
+ unnormalized_dual_quat).as_dual_quat()
499
+ xp_assert_close(dual_quat[:4], xp.asarray([0.0, 0, 0, 1]), atol=1e-12)
500
+ xp_assert_close(xp.vecdot(dual_quat[:4], dual_quat[4:])[()], xp.asarray(0.0)[()],
501
+ atol=1e-12)
502
+
503
+ # compensation for precision loss in real quaternion
504
+ rng = np.random.default_rng(1000)
505
+ t = xp.asarray(rng.normal(size=(3,)))
506
+ r = rotation_to_xp(Rotation.random(10, rng=rng), xp=xp)
507
+ random_dual_quats = RigidTransform.from_components(t, r).as_dual_quat()
508
+
509
+ # ensure that random quaternions are not normalized
510
+ random_dual_quats = xpx.at(random_dual_quats)[:, :4].add(0.01)
511
+ assert not xp.any(xpx.isclose(xp_vector_norm(random_dual_quats[:, :4], axis=1),
512
+ 1.0, atol=0.0001))
513
+ dual_quat_norm = RigidTransform.from_dual_quat(
514
+ random_dual_quats).as_dual_quat()
515
+ expected = xp.ones(dual_quat_norm.shape[0])
516
+ xp_assert_close(xp_vector_norm(dual_quat_norm[:, :4], axis=1), expected, atol=1e-12)
517
+
518
+ # compensation for precision loss in dual quaternion, results in violation
519
+ # of orthogonality constraint
520
+ t = xp.asarray(rng.normal(size=(10, 3)))
521
+ r = rotation_to_xp(Rotation.random(10, rng=rng), xp=xp)
522
+ random_dual_quats = RigidTransform.from_components(t, r).as_dual_quat()
523
+
524
+ # ensure that random quaternions are not normalized
525
+ random_dual_quats = xpx.at(random_dual_quats)[:, 4:].add(0.01)
526
+ q_norm = xp.vecdot(random_dual_quats[:, :4], random_dual_quats[:, 4:])
527
+ assert not xp.any(xpx.isclose(q_norm, 0.0, atol=0.0001))
528
+ dual_quat_norm = RigidTransform.from_dual_quat(
529
+ random_dual_quats).as_dual_quat()
530
+ expected = xp.zeros(dual_quat_norm.shape[0])
531
+ xp_assert_close(xp.vecdot(dual_quat_norm[:, :4], dual_quat_norm[:, 4:]), expected,
532
+ atol=1e-12)
533
+ xp_assert_close(random_dual_quats[:, :4], dual_quat_norm[:, :4], atol=1e-12)
534
+
535
+
536
+ def test_from_dual_quat_array_like():
537
+ rng = np.random.default_rng(123)
538
+ # Test single transform
539
+ t = np.array([1, 2, 3])
540
+ r = Rotation.random(rng=rng)
541
+ tf_expected = RigidTransform.from_components(t, r)
542
+ dual_quat = tf_expected.as_dual_quat().tolist()
543
+ assert isinstance(dual_quat, list)
544
+ tf = RigidTransform.from_dual_quat(dual_quat)
545
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12)
546
+
547
+ # Test multiple transforms
548
+ t = [[1, 2, 3], [4, 5, 6]]
549
+ r = Rotation.random(len(t), rng=rng)
550
+ tf_expected = RigidTransform.from_components(t, r)
551
+ dual_quat = tf_expected.as_dual_quat().tolist()
552
+ assert isinstance(dual_quat, list)
553
+ tf = RigidTransform.from_dual_quat(dual_quat)
554
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12)
555
+
556
+
557
+ def test_as_dual_quat(xp):
558
+ # identity
559
+ expected = xp.asarray([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0])
560
+ actual = xp.asarray(RigidTransform.identity().as_dual_quat())
561
+ xp_assert_close(actual, expected, atol=1e-12)
562
+
563
+ expected = xp.asarray([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
564
+ actual = xp.asarray(RigidTransform.identity().as_dual_quat(scalar_first=True))
565
+ xp_assert_close(actual, expected, atol=1e-12)
566
+
567
+ rng = np.random.default_rng(10)
568
+
569
+ # only rotation
570
+ for _ in range(10):
571
+ real_part = xp.asarray(Rotation.random(rng=rng).as_quat())
572
+ dual_part = xp.zeros(4)
573
+ expected = xp.concat((real_part, dual_part), axis=-1)
574
+ actual = RigidTransform.from_dual_quat(expected).as_dual_quat()
575
+ # because of double cover:
576
+ if xp.sign(expected[0]) != xp.sign(actual[0]):
577
+ actual = -actual
578
+ xp_assert_close(actual, expected, atol=1e-12)
579
+
580
+ # only translation
581
+ for _ in range(10):
582
+ tf = 0.5 * rng.normal(size=3)
583
+ expected = xp.asarray([0.0, 0, 0, 1, *tf.tolist(), 0])
584
+ actual = RigidTransform.from_dual_quat(expected).as_dual_quat()
585
+ # because of double cover:
586
+ if xp.sign(expected[0]) != xp.sign(actual[0]):
587
+ actual = -actual
588
+ xp_assert_close(actual, expected, atol=1e-12)
589
+
590
+ # rotation and translation
591
+ for _ in range(10):
592
+ t = xp.asarray(rng.normal(size=3))
593
+ r = rotation_to_xp(Rotation.random(rng=rng), xp=xp)
594
+ expected = RigidTransform.from_components(t, r).as_dual_quat()
595
+ actual = RigidTransform.from_dual_quat(expected).as_dual_quat()
596
+ # because of double cover:
597
+ if xp.sign(expected[0]) != xp.sign(actual[0]):
598
+ actual = -actual
599
+ xp_assert_close(actual, expected, atol=1e-12)
600
+
601
+
602
+ def test_from_as_internal_consistency(xp):
603
+ atol = 1e-12
604
+ n = 1000
605
+ rng = np.random.default_rng(10)
606
+ t = xp.asarray(rng.normal(size=(n, 3)))
607
+ r = rotation_to_xp(Rotation.random(n, rng=rng), xp=xp)
608
+ tf0 = RigidTransform.from_components(t, r)
609
+
610
+ tf1 = RigidTransform.from_components(*tf0.as_components())
611
+ xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol)
612
+
613
+ tf1 = RigidTransform.from_components(tf0.translation, tf0.rotation)
614
+ xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol)
615
+
616
+ tf1 = RigidTransform.from_exp_coords(tf0.as_exp_coords())
617
+ xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol)
618
+
619
+ tf1 = RigidTransform.from_matrix(tf0.as_matrix())
620
+ xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol)
621
+
622
+ tf1 = RigidTransform.from_dual_quat(tf0.as_dual_quat())
623
+ xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol)
624
+
625
+ # exp_coords small rotation
626
+ tf0 = RigidTransform.from_components(
627
+ xp.asarray(rng.normal(scale=1000.0, size=(1000, 3))),
628
+ Rotation.from_rotvec(xp.asarray(rng.normal(scale=1e-10, size=(1000, 3)))))
629
+ tf1 = RigidTransform.from_exp_coords(tf0.as_exp_coords())
630
+ xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol)
631
+
632
+
633
+ def test_identity():
634
+ # We do not use xp here because identity always returns numpy arrays
635
+ atol = 1e-12
636
+
637
+ # Test single identity
638
+ tf = RigidTransform.identity()
639
+ xp_assert_close(tf.as_matrix(), np.eye(4), atol=atol)
640
+
641
+ # Test multiple identities
642
+ tf = RigidTransform.identity(5)
643
+ xp_assert_close(tf.as_matrix(), np.array([np.eye(4)] * 5), atol=atol)
644
+
645
+
646
+ def test_apply(xp):
647
+ atol = 1e-12
648
+
649
+ ## Single transform
650
+ r = Rotation.from_euler('z', xp.asarray(90), degrees=True)
651
+ t = xp.asarray([2, 3, 4])
652
+ tf = RigidTransform.from_components(t, r)
653
+
654
+ # Single vector, single transform
655
+ vec = xp.asarray([1, 0, 0])
656
+ expected = t + r.apply(vec)
657
+ res = tf.apply(vec)
658
+ xp_assert_close(res, expected, atol=atol)
659
+
660
+ # Multiple vectors, single transform
661
+ vecs = xp.asarray([[1, 0, 0], [0, 1, 0]])
662
+ expected = t + r.apply(vecs)
663
+ xp_assert_close(tf.apply(vecs), expected, atol=atol)
664
+
665
+ ## Multiple transforms
666
+ r = Rotation.from_euler('z', xp.asarray([90, 0]), degrees=True)
667
+ t = xp.asarray([[2, 3, 4], [5, 6, 7]])
668
+ tf = RigidTransform.from_components(t, r)
669
+
670
+ # Single vector, multiple transforms
671
+ vec = xp.asarray([1, 0, 0])
672
+ expected = t + r.apply(vec)
673
+ xp_assert_close(tf.apply(vec), expected, atol=atol)
674
+
675
+ # Multiple vectors, multiple transforms
676
+ vecs = xp.asarray([[1, 0, 0], [0, 1, 0]])
677
+ expected = t + r.apply(vecs)
678
+ xp_assert_close(tf.apply(vecs), expected, atol=atol)
679
+
680
+
681
+ def test_apply_array_like():
682
+ rng = np.random.default_rng(123)
683
+ # Single vector
684
+ t = np.array([1, 2, 3])
685
+ r = Rotation.random(rng=rng)
686
+ tf = RigidTransform.from_components(t, r)
687
+ vec = [1, 0, 0]
688
+ expected = t + r.apply(vec)
689
+ xp_assert_close(tf.apply(vec), expected, atol=1e-12)
690
+
691
+ # Multiple vectors
692
+ t = np.array([[1, 2, 3], [4, 5, 6]])
693
+ r = Rotation.random(len(t), rng=rng)
694
+ tf = RigidTransform.from_components(t, r)
695
+ vec = [[1, 0, 0], [0, 1, 0]]
696
+ expected = t + r.apply(vec)
697
+ xp_assert_close(tf.apply(vec), expected, atol=1e-12)
698
+
699
+
700
+ def test_inverse_apply(xp):
701
+ atol = 1e-12
702
+
703
+ # Test applying inverse transform
704
+ t = xp.asarray([1, 2, 3])
705
+ r = Rotation.from_euler('z', xp.asarray(90), degrees=True)
706
+ tf = RigidTransform.from_components(t, r)
707
+
708
+ # Test single vector
709
+ vec = xp.asarray([1, 0, 0])
710
+ expected = tf.inv().apply(vec)
711
+ xp_assert_close(tf.apply(vec, inverse=True), expected, atol=atol)
712
+
713
+ # Test multiple vectors
714
+ vecs = xp.asarray([[1, 0, 0], [0, 1, 0]])
715
+ expected = tf.inv().apply(vecs)
716
+ xp_assert_close(tf.apply(vecs, inverse=True), expected, atol=atol)
717
+
718
+
719
+ def test_rotation_alone(xp):
720
+ atol = 1e-12
721
+
722
+ r = Rotation.from_euler('z', xp.asarray(90), degrees=True)
723
+ tf = RigidTransform.from_rotation(r)
724
+ vec = xp.asarray([1, 0, 0])
725
+ expected = r.apply(vec)
726
+ xp_assert_close(tf.apply(vec), expected, atol=atol)
727
+
728
+
729
+ def test_translation_alone(xp):
730
+ atol = 1e-12
731
+ t = xp.asarray([1.0, 2, 3])
732
+ tf = RigidTransform.from_translation(t)
733
+ vec = xp.asarray([5.0, 6, 7])
734
+ expected = t + vec
735
+ xp_assert_close(tf.apply(vec), expected, atol=atol)
736
+
737
+
738
+ def test_composition(xp):
739
+ atol = 1e-12
740
+
741
+ # Test composing single transforms
742
+ t1 = xp.asarray([1.0, 0, 0])
743
+ r1 = Rotation.from_euler('z', xp.asarray(90), degrees=True)
744
+ tf1 = RigidTransform.from_components(t1, r1)
745
+
746
+ t2 = xp.asarray([0, 1, 0])
747
+ r2 = Rotation.from_euler('x', xp.asarray(90), degrees=True)
748
+ tf2 = RigidTransform.from_components(t2, r2)
749
+
750
+ composed = tf2 * tf1
751
+ vec = xp.asarray([1, 0, 0])
752
+ expected = tf2.apply(tf1.apply(vec))
753
+ xp_assert_close(composed.apply(vec), expected, atol=atol)
754
+ assert composed.single
755
+
756
+ expected = t2 + r2.apply(t1 + r1.apply(vec))
757
+ xp_assert_close(composed.apply(vec), expected, atol=atol)
758
+
759
+ # Multiple transforms with single transform
760
+ t2 = np.array([[1, 2, 3], [4, 5, 6]])
761
+ tf2 = RigidTransform.from_components(t2, r2)
762
+
763
+ composed = tf2 * tf1
764
+ expected = tf2.apply(tf1.apply(vec))
765
+ xp_assert_close(composed.apply(vec), expected, atol=atol)
766
+ assert not composed.single
767
+
768
+ expected = t2 + r2.apply(t1 + r1.apply(vec))
769
+ xp_assert_close(composed.apply(vec), expected, atol=atol)
770
+
771
+ # Multiple transforms with multiple transforms
772
+ t1 = xp.asarray([[1, 0, 0], [0, -1, 1]])
773
+ tf1 = RigidTransform.from_components(t1, r1)
774
+
775
+ composed = tf2 * tf1
776
+ expected = tf2.apply(tf1.apply(vec))
777
+ xp_assert_close(composed.apply(vec), expected, atol=atol)
778
+ assert not composed.single
779
+
780
+ expected = t2 + r2.apply(t1 + r1.apply(vec))
781
+ xp_assert_close(composed.apply(vec), expected, atol=atol)
782
+
783
+
784
+ def test_pow(xp):
785
+ atol = 1e-12
786
+ num = 10
787
+ rng = np.random.default_rng(100)
788
+ t = xp.asarray(rng.normal(size=(num, 3)))
789
+ r = rotation_to_xp(Rotation.random(num, rng=rng), xp=xp)
790
+ p = RigidTransform.from_components(t, r)
791
+ p_inv = p.inv()
792
+
793
+ # Test the short-cuts and other integers
794
+ for n in [-5, -2, -1, 0, 1, 2, 5]:
795
+ q = p**n
796
+ r = rigid_transform_to_xp(RigidTransform.identity(num), xp=xp)
797
+ for _ in range(abs(n)):
798
+ if n > 0:
799
+ r = r * p
800
+ else:
801
+ r = r * p_inv
802
+ xp_assert_close(q.as_matrix(), r.as_matrix(), atol=atol)
803
+
804
+ # Test shape preservation
805
+ r = RigidTransform.from_rotation(Rotation.from_quat(xp.asarray([0, 0, 0, 1])))
806
+ assert (r**n).as_matrix().shape == (4, 4)
807
+ r = RigidTransform.from_rotation(Rotation.from_quat(xp.asarray([[0, 0, 0, 1]])))
808
+ assert (r**n).as_matrix().shape == (1, 4, 4)
809
+
810
+ # Test fractional powers
811
+ q = p**0.5
812
+ xp_assert_close((q * q).as_matrix(), p.as_matrix(), atol=atol)
813
+ q = p**-0.5
814
+ xp_assert_close((q * q).as_matrix(), p.inv().as_matrix(), atol=atol)
815
+ q = p** 1.5
816
+ xp_assert_close((q * q).as_matrix(), (p**3).as_matrix(), atol=atol)
817
+ q = p** -1.5
818
+ xp_assert_close((q * q).as_matrix(), (p**-3).as_matrix(), atol=atol)
819
+
820
+ # pow function
821
+ tf = pow(RigidTransform.from_matrix(xp.eye(4)), 2)
822
+ xp_assert_close(tf.as_matrix(), xp.eye(4), atol=atol)
823
+
824
+
825
+ def test_pow_equivalence_with_rotation(xp):
826
+ atol = 1e-12
827
+ num = 10
828
+ rng = np.random.default_rng(100)
829
+ r = rotation_to_xp(Rotation.random(num, rng=rng), xp=xp)
830
+ p = RigidTransform.from_rotation(r)
831
+ for n in [-5, -2, -1.5, -1, -0.5, 0.0, 0.5, 1, 1.5, 2, 5]:
832
+ xp_assert_close((p**n).rotation.as_matrix(), (r**n).as_matrix(), atol=atol)
833
+
834
+
835
+ def test_inverse(xp):
836
+ atol = 1e-12
837
+
838
+ # Test inverse transform
839
+ r = Rotation.from_euler('z', xp.asarray(90), degrees=True)
840
+ t = xp.asarray([1, 2, 3])
841
+ tf = RigidTransform.from_components(t, r)
842
+
843
+ # Test that tf * tf.inv() equals identity
844
+ tf_inv = tf.inv()
845
+ composed = tf * tf_inv
846
+ xp_assert_close(composed.as_matrix(), xp.eye(4), atol=atol)
847
+
848
+ n = 10
849
+ rng = np.random.default_rng(1000)
850
+ t = xp.asarray(rng.normal(size=(n, 3)))
851
+ r = rotation_to_xp(Rotation.random(n, rng=rng), xp=xp)
852
+ tf = RigidTransform.from_components(t, r)
853
+ tf_inv = tf.inv()
854
+ composed = tf * tf_inv
855
+ expected = xp.repeat(xp.eye(4)[None, ...], n, axis=0)
856
+ xp_assert_close(composed.as_matrix(), expected, atol=atol)
857
+
858
+ # Test multiple transforms
859
+ r = Rotation.from_euler('zyx', xp.asarray([[90, 0, 0], [0, 90, 0]]), degrees=True)
860
+ t = xp.asarray([[1, 2, 3], [4, 5, 6]])
861
+ tf = RigidTransform.from_components(t, r)
862
+ tf_inv = tf.inv()
863
+ composed = tf * tf_inv
864
+ expected = xp.repeat(xp.eye(4)[None, ...], 2, axis=0)
865
+ xp_assert_close(composed.as_matrix(), expected, atol=atol)
866
+
867
+
868
+ def test_properties(xp):
869
+ atol = 1e-12
870
+
871
+ # Test rotation and translation properties for single transform
872
+ r = Rotation.from_euler('z', xp.asarray(90), degrees=True)
873
+ t = xp.asarray([1.0, 2, 3])
874
+ tf = RigidTransform.from_components(t, r)
875
+
876
+ xp_assert_close(tf.rotation.as_matrix(), r.as_matrix(), atol=atol)
877
+ assert tf.rotation.approx_equal(r)
878
+ xp_assert_close(tf.translation, t, atol=atol)
879
+
880
+ # Test rotation and translation properties for multiple transforms
881
+ r = Rotation.from_euler('zyx', xp.asarray([[90, 0, 0], [0, 90, 0]]), degrees=True)
882
+ t = xp.asarray([[1.0, 2, 3], [4, 5, 6]])
883
+ tf = RigidTransform.from_components(t, r)
884
+
885
+ xp_assert_close(tf.rotation.as_matrix(), r.as_matrix(), atol=atol)
886
+ assert all(tf.rotation.approx_equal(r))
887
+ xp_assert_close(tf.translation, t, atol=atol)
888
+
889
+
890
+ def test_indexing(xp):
891
+ atol = 1e-12
892
+
893
+ # Test indexing for multiple transforms
894
+ r = Rotation.from_euler('zyx', xp.asarray([[90, 0, 0], [0, 90, 0]]), degrees=True)
895
+ t = xp.asarray([[1.0, 2, 3], [4, 5, 6]])
896
+ tf = RigidTransform.from_components(t, r)
897
+
898
+ # Test single index
899
+ xp_assert_close(tf[0].as_matrix()[:3, :3], r[0].as_matrix(), atol=atol)
900
+ xp_assert_close(tf[0].as_matrix()[:3, 3], t[0, ...], atol=atol)
901
+
902
+ # Test slice
903
+ tf_slice = tf[0:2]
904
+ xp_assert_close(tf_slice.as_matrix()[:, :3, :3], r[0:2].as_matrix(), atol=atol)
905
+ xp_assert_close(tf_slice.as_matrix()[:, :3, 3], t[0:2, ...], atol=atol)
906
+
907
+ # Test boolean indexing
908
+ tf_masked = tf[xp.asarray([True, True])]
909
+ xp_assert_close(tf_masked.as_matrix()[:, :3, :3], r.as_matrix(), atol=atol)
910
+ xp_assert_close(tf_masked.as_matrix()[:, :3, 3], t, atol=atol)
911
+
912
+ tf_masked = tf[xp.asarray([False, True])]
913
+ xp_assert_close(tf_masked.as_matrix()[:, :3, :3],
914
+ r[xp.asarray([False, True])].as_matrix(), atol=atol)
915
+ xp_assert_close(tf_masked.as_matrix()[:, :3, 3], t[xp.asarray([False, True])],
916
+ atol=atol)
917
+
918
+ tf_masked = tf[xp.asarray([False, False])]
919
+ assert len(tf_masked) == 0
920
+
921
+
922
+ def test_indexing_array_like():
923
+ atol = 1e-12
924
+
925
+ r = Rotation.from_euler('zyx', np.array([[90, 0, 0], [0, 90, 0]]), degrees=True)
926
+ t = np.array([[1.0, 2, 3], [4, 5, 6]])
927
+ tf = RigidTransform.from_components(t, r)
928
+
929
+ tf_masked = tf[[False, True]]
930
+ xp_assert_close(tf_masked.as_matrix()[:, :3, :3], r[[False, True]].as_matrix(),
931
+ atol=atol)
932
+ xp_assert_close(tf_masked.as_matrix()[:, :3, 3], t[[False, True]], atol=atol)
933
+ tf_masked = tf[[False, False]]
934
+ assert len(tf_masked) == 0
935
+
936
+
937
+ def test_concatenate(xp):
938
+ atol = 1e-12
939
+
940
+ # Test concatenation of transforms
941
+ t1 = xp.asarray([1, 0, 0])
942
+ r1 = Rotation.from_euler('z', xp.asarray(90), degrees=True)
943
+ tf1 = RigidTransform.from_components(t1, r1)
944
+
945
+ t2 = xp.asarray([0, 1, 0])
946
+ r2 = Rotation.from_euler('x', xp.asarray(90), degrees=True)
947
+ tf2 = RigidTransform.from_components(t2, r2)
948
+
949
+ # Concatenate single transforms
950
+ concatenated1 = RigidTransform.concatenate([tf1, tf2])
951
+ xp_assert_close(concatenated1[0].as_matrix(), tf1.as_matrix(), atol=atol)
952
+ xp_assert_close(concatenated1[1].as_matrix(), tf2.as_matrix(), atol=atol)
953
+
954
+ # Concatenate multiple transforms
955
+ concatenated2 = RigidTransform.concatenate([tf1, concatenated1])
956
+ xp_assert_close(concatenated2[0].as_matrix(), tf1.as_matrix(), atol=atol)
957
+ xp_assert_close(concatenated2[1].as_matrix(), tf1.as_matrix(), atol=atol)
958
+ xp_assert_close(concatenated2[2].as_matrix(), tf2.as_matrix(), atol=atol)
959
+
960
+
961
+ def test_input_validation(xp):
962
+ # Test invalid matrix shapes
963
+ inputs = [xp.eye(3), xp.zeros((4, 3)), [], xp.zeros((1, 1, 4, 4))]
964
+ for input in inputs:
965
+ with pytest.raises(ValueError, match="Expected `matrix` to have shape"):
966
+ RigidTransform.from_matrix(input)
967
+
968
+ # Test invalid last row
969
+ matrix = xp.eye(4)
970
+ matrix = xpx.at(matrix)[3, :].set(xp.asarray([1, 0, 0, 1]))
971
+ if is_lazy_array(matrix):
972
+ matrix = RigidTransform.from_matrix(matrix).as_matrix()
973
+ assert xp.all(xp.isnan(matrix))
974
+ else:
975
+ with pytest.raises(ValueError, match="last row of transformation matrix 0"):
976
+ RigidTransform.from_matrix(matrix)
977
+
978
+ # Test invalid last row for multiple transforms
979
+ matrix = xp.zeros((2, 4, 4))
980
+ matrix = xpx.at(matrix)[...].set(xp.eye(4))
981
+ matrix = xpx.at(matrix)[1, 3, :].set(xp.asarray([1, 0, 0, 1]))
982
+ if is_lazy_array(matrix):
983
+ matrix = RigidTransform.from_matrix(matrix).as_matrix()
984
+ assert not xp.any(xp.isnan(matrix[0, ...]))
985
+ assert xp.all(xp.isnan(matrix[1, ...]))
986
+ else:
987
+ with pytest.raises(ValueError, match="last row of transformation matrix 1"):
988
+ RigidTransform.from_matrix(matrix)
989
+
990
+ # Test left handed rotation matrix
991
+ matrix = xp.eye(4)
992
+ matrix = xpx.at(matrix)[0, 0].set(-1)
993
+ if is_lazy_array(matrix):
994
+ matrix = RigidTransform.from_matrix(matrix).as_matrix()
995
+ assert xp.all(xp.isnan(matrix[..., :3, :3]))
996
+ else:
997
+ with pytest.raises(ValueError, match="Non-positive determinant"):
998
+ RigidTransform(matrix, normalize=True)
999
+
1000
+ # Test non-Rotation input
1001
+ with pytest.raises(ValueError,
1002
+ match="Expected `rotation` to be a `Rotation` instance"):
1003
+ RigidTransform.from_rotation(xp.eye(3))
1004
+
1005
+
1006
+ def test_translation_validation(xp):
1007
+ # Test invalid translation shapes
1008
+ with pytest.raises(ValueError, match="Expected `translation` to have shape"):
1009
+ RigidTransform.from_translation(xp.asarray([1, 2]))
1010
+
1011
+ with pytest.raises(ValueError, match="Expected `translation` to have shape"):
1012
+ RigidTransform.from_translation(xp.zeros((2, 2)))
1013
+
1014
+ with pytest.raises(ValueError, match="Expected `translation` to have shape"):
1015
+ RigidTransform.from_translation(xp.zeros((1, 1, 3)))
1016
+
1017
+
1018
+ def test_vector_validation(xp):
1019
+ tf = rigid_transform_to_xp(RigidTransform.identity(2), xp=xp)
1020
+
1021
+ # Test invalid vector shapes
1022
+ with pytest.raises(ValueError, match="Expected vector to have shape"):
1023
+ tf.apply(xp.asarray([1, 2]))
1024
+
1025
+ with pytest.raises(ValueError, match="Expected vector to have shape"):
1026
+ tf.apply(xp.zeros((2, 2)))
1027
+
1028
+ with pytest.raises(ValueError, match="Expected vector to have shape"):
1029
+ tf.apply(xp.zeros((1, 1, 3)))
1030
+
1031
+
1032
+ def test_indexing_validation(xp):
1033
+ tf = RigidTransform.from_matrix(xp.eye(4))
1034
+
1035
+ # Test indexing on single transform
1036
+ with pytest.raises(TypeError, match="Single transform is not subscriptable"):
1037
+ tf[0]
1038
+
1039
+ with pytest.raises(TypeError, match="Single transform is not subscriptable"):
1040
+ tf[0:1]
1041
+
1042
+ # Test length on single transform
1043
+ with pytest.raises(TypeError, match="Single transform has no len"):
1044
+ len(tf)
1045
+
1046
+
1047
+ def test_composition_validation(xp):
1048
+ tf2 = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6]]))
1049
+ tf3 = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]]))
1050
+
1051
+ # Test incompatible shapes
1052
+ with pytest.raises(ValueError, match="Expected equal number of transforms"):
1053
+ tf2 * tf3
1054
+
1055
+
1056
+ def test_concatenate_validation(xp):
1057
+ tf = RigidTransform.from_matrix(xp.eye(4))
1058
+
1059
+ # Test invalid inputs
1060
+ with pytest.raises(TypeError,
1061
+ match="input must contain RigidTransform objects"):
1062
+ RigidTransform.concatenate([tf, xp.eye(4)])
1063
+
1064
+
1065
+ def test_setitem_validation(xp):
1066
+ tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6]]))
1067
+ single = RigidTransform.from_matrix(xp.eye(4))
1068
+
1069
+ # Test setting item on single transform
1070
+ with pytest.raises(TypeError, match="Single transform is not subscriptable"):
1071
+ single[0] = tf
1072
+
1073
+ # Test invalid value type
1074
+ with pytest.raises(TypeError, match="value must be a RigidTransform"):
1075
+ tf[0] = xp.eye(4)
1076
+
1077
+
1078
+ @pytest.mark.skip_xp_backends("jax.numpy",
1079
+ reason="JAX does not support memory sharing")
1080
+ def test_copy_flag(xp):
1081
+ # Test that copy=True creates new memory
1082
+ matrix = xp.eye(4)
1083
+ tf = RigidTransform(matrix, normalize=False, copy=True)
1084
+ matrix[0, 0] = 2
1085
+ assert tf.as_matrix()[0, 0] == 1
1086
+
1087
+ # Test that copy=False shares memory
1088
+ matrix = xp.eye(4)
1089
+ tf = RigidTransform(matrix, normalize=False, copy=False)
1090
+ matrix[0, 0] = 2
1091
+ assert tf.as_matrix()[0, 0] == 2
1092
+
1093
+
1094
+ def test_normalize_dual_quaternion(xp):
1095
+ dual_quat = normalize_dual_quaternion(xp.zeros((1, 8)))
1096
+ xp_assert_close(xp_vector_norm(dual_quat[0, :4], axis=-1), xp.asarray(1.0)[()],
1097
+ atol=1e-12)
1098
+ xp_assert_close(xp.vecdot(dual_quat[0, :4], dual_quat[0, 4:])[()],
1099
+ xp.asarray(0.0)[()], atol=1e-12)
1100
+
1101
+ rng = np.random.default_rng(103213650)
1102
+ dual_quat = xp.asarray(rng.normal(size=(1000, 8)))
1103
+ dual_quat = normalize_dual_quaternion(dual_quat)
1104
+ expected = xp.ones(dual_quat.shape[0])
1105
+ xp_assert_close(xp_vector_norm(dual_quat[:, :4], axis=-1), expected, atol=1e-12)
1106
+ expected = xp.zeros(dual_quat.shape[0])
1107
+ xp_assert_close(xp.vecdot(dual_quat[:, :4], dual_quat[:, 4:]), expected, atol=1e-12)
1108
+
1109
+
1110
+ def test_empty_transform_construction(xp):
1111
+ tf = RigidTransform.from_matrix(xp.empty((0, 4, 4)))
1112
+ assert len(tf) == 0
1113
+ assert not tf.single
1114
+
1115
+ tf = RigidTransform.from_rotation(Rotation.from_quat(xp.zeros((0, 4))))
1116
+ assert len(tf) == 0
1117
+ assert not tf.single
1118
+
1119
+ tf = RigidTransform.from_translation(xp.empty((0, 3)))
1120
+ assert len(tf) == 0
1121
+ assert not tf.single
1122
+
1123
+ empty_rot = Rotation.from_quat(xp.zeros((0, 4)))
1124
+ tf = RigidTransform.from_components(xp.empty((0, 3)), empty_rot)
1125
+ assert len(tf) == 0
1126
+ assert not tf.single
1127
+
1128
+ tf = RigidTransform.from_exp_coords(xp.empty((0, 6)))
1129
+ assert len(tf) == 0
1130
+ assert not tf.single
1131
+
1132
+ tf = RigidTransform.from_dual_quat(xp.empty((0, 8)))
1133
+ assert len(tf) == 0
1134
+ assert not tf.single
1135
+
1136
+ tf = RigidTransform.identity(0)
1137
+ assert len(tf) == 0
1138
+ assert not tf.single
1139
+
1140
+
1141
+ def test_empty_transform_representation(xp):
1142
+ tf = RigidTransform.from_matrix(xp.empty((0, 4, 4)))
1143
+
1144
+ assert len(tf.rotation) == 0
1145
+ assert tf.translation.shape == (0, 3)
1146
+
1147
+ t, r = tf.as_components()
1148
+ assert t.shape == (0, 3)
1149
+ assert len(r) == 0
1150
+
1151
+ assert tf.as_matrix().shape == (0, 4, 4)
1152
+ assert tf.as_exp_coords().shape == (0, 6)
1153
+ assert tf.as_dual_quat().shape == (0, 8)
1154
+
1155
+
1156
+ def test_empty_transform_application(xp):
1157
+ tf = RigidTransform.from_matrix(xp.empty((0, 4, 4)))
1158
+
1159
+ assert tf.apply(xp.zeros((3,))).shape == (0, 3)
1160
+ assert tf.apply(xp.empty((0, 3))).shape == (0, 3)
1161
+
1162
+ with pytest.raises(ValueError, match="operands could not be broadcast together"):
1163
+ tf.apply(xp.zeros((2, 3)))
1164
+
1165
+
1166
+ def test_empty_transform_composition(xp):
1167
+ tf_empty = RigidTransform.from_matrix(xp.empty((0, 4, 4)))
1168
+ tf_single = RigidTransform.from_matrix(xp.eye(4))
1169
+ tf_many = rigid_transform_to_xp(RigidTransform.identity(3), xp=xp)
1170
+
1171
+ assert len(tf_empty * tf_empty) == 0
1172
+ assert len(tf_empty * tf_single) == 0
1173
+ assert len(tf_single * tf_empty) == 0
1174
+
1175
+ with pytest.raises(ValueError, match="Expected equal number of transforms"):
1176
+ tf_many * tf_empty
1177
+
1178
+ with pytest.raises(ValueError, match="Expected equal number of transforms"):
1179
+ tf_empty * tf_many
1180
+
1181
+
1182
+ def test_empty_transform_concatenation(xp):
1183
+ tf_empty = RigidTransform.from_matrix(xp.empty((0, 4, 4)))
1184
+ tf_single = RigidTransform.from_matrix(xp.eye(4))
1185
+ tf_many = rigid_transform_to_xp(RigidTransform.identity(2), xp=xp)
1186
+
1187
+ assert len(RigidTransform.concatenate([tf_empty, tf_empty])) == 0
1188
+ assert len(RigidTransform.concatenate([tf_empty, tf_single])) == 1
1189
+ assert len(RigidTransform.concatenate([tf_single, tf_empty])) == 1
1190
+ assert len(RigidTransform.concatenate([tf_empty, tf_many])) == 2
1191
+ assert len(RigidTransform.concatenate([tf_many, tf_empty])) == 2
1192
+ assert len(RigidTransform.concatenate([tf_many, tf_empty, tf_single])) == 3
1193
+
1194
+
1195
+ def test_empty_transform_inv_and_pow(xp):
1196
+ tf = RigidTransform.from_matrix(xp.empty((0, 4, 4)))
1197
+ assert len(tf.inv()) == 0
1198
+ assert len(tf ** 0) == 0
1199
+ assert len(tf ** 1) == 0
1200
+ assert len(tf ** -1) == 0
1201
+ assert len(tf ** 0.5) == 0
1202
+
1203
+
1204
+ def test_empty_transform_indexing(xp):
1205
+ tf_many = rigid_transform_to_xp(RigidTransform.identity(3), xp=xp)
1206
+ tf_zero = tf_many[xp.asarray([], dtype=xp.int64)]
1207
+ assert len(tf_zero) == 0
1208
+
1209
+ assert len(tf_zero[xp.asarray([], dtype=xp.int64)]) == 0
1210
+ # Array API does not specify out-of-bounds indexing. Only check for numpy.
1211
+ if is_numpy(xp):
1212
+ assert len(tf_zero[:5]) == 0 # Slices can go out of bounds.
1213
+
1214
+ with pytest.raises(IndexError):
1215
+ tf_zero[0]
1216
+
1217
+ with pytest.raises(IndexError):
1218
+ tf_zero[xp.asarray([0, 2])]
1219
+
1220
+ with pytest.raises(IndexError):
1221
+ tf_zero[xp.asarray([False, True])]