Eric-Yyvuw commited on
Commit
a204baa
·
verified ·
1 Parent(s): 19f2d7e

sync 2026-10-29 07:40 GMT (part 7)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. v12x/pylib/scipy-1.18.1.dist-info/sboms/auditwheel.cdx.json +1 -0
  2. v12x/pylib/scipy/spatial/transform/__init__.py +31 -0
  3. v12x/pylib/scipy/spatial/transform/_rigid_transform.py +1975 -0
  4. v12x/pylib/scipy/spatial/transform/_rigid_transform_xp.py +448 -0
  5. v12x/pylib/scipy/spatial/transform/_rotation.py +0 -0
  6. v12x/pylib/scipy/spatial/transform/_rotation_groups.py +140 -0
  7. v12x/pylib/scipy/spatial/transform/_rotation_spline.py +465 -0
  8. v12x/pylib/scipy/spatial/transform/_rotation_xp.py +1140 -0
  9. v12x/pylib/scipy/spatial/transform/rotation.py +21 -0
  10. v12x/pylib/scipy/spatial/transform/tests/__init__.py +0 -0
  11. v12x/pylib/scipy/spatial/transform/tests/test_rigid_transform.py +1531 -0
  12. v12x/pylib/scipy/spatial/transform/tests/test_rotation.py +0 -0
  13. v12x/pylib/scipy/spatial/transform/tests/test_rotation_groups.py +169 -0
  14. v12x/pylib/scipy/spatial/transform/tests/test_rotation_spline.py +188 -0
  15. v12x/pylib/scipy/special/__init__.py +842 -0
  16. v12x/pylib/scipy/special/_add_newdocs.py +0 -0
  17. v12x/pylib/scipy/special/_basic.py +0 -0
  18. v12x/pylib/scipy/special/_ellip_harm.py +214 -0
  19. v12x/pylib/scipy/special/_input_validation.py +17 -0
  20. v12x/pylib/scipy/special/_lambertw.py +149 -0
  21. v12x/pylib/scipy/special/_logsumexp.py +420 -0
  22. v12x/pylib/scipy/special/_mptestutils.py +453 -0
  23. v12x/pylib/scipy/special/_multiufuncs.py +744 -0
  24. v12x/pylib/scipy/special/_orthogonal.py +2856 -0
  25. v12x/pylib/scipy/special/_precompute/__init__.py +0 -0
  26. v12x/pylib/scipy/special/_precompute/cosine_cdf.py +17 -0
  27. v12x/pylib/scipy/special/_precompute/expn_asy.py +54 -0
  28. v12x/pylib/scipy/special/_precompute/gammainc_asy.py +116 -0
  29. v12x/pylib/scipy/special/_precompute/gammainc_data.py +124 -0
  30. v12x/pylib/scipy/special/_precompute/hyp2f1_data.py +484 -0
  31. v12x/pylib/scipy/special/_precompute/lambertw.py +68 -0
  32. v12x/pylib/scipy/special/_precompute/loggamma.py +43 -0
  33. v12x/pylib/scipy/special/_precompute/struve_convergence.py +131 -0
  34. v12x/pylib/scipy/special/_precompute/utils.py +38 -0
  35. v12x/pylib/scipy/special/_precompute/wright_bessel.py +342 -0
  36. v12x/pylib/scipy/special/_precompute/wright_bessel_data.py +152 -0
  37. v12x/pylib/scipy/special/_precompute/wrightomega.py +41 -0
  38. v12x/pylib/scipy/special/_precompute/zetac.py +27 -0
  39. v12x/pylib/scipy/special/_sf_error.py +15 -0
  40. v12x/pylib/scipy/special/_spfun_stats.py +178 -0
  41. v12x/pylib/scipy/special/_spherical_bessel.py +401 -0
  42. v12x/pylib/scipy/special/_support_alternative_backends.py +933 -0
  43. v12x/pylib/scipy/special/_testutils.py +321 -0
  44. v12x/pylib/scipy/special/_ufunc_tools.py +162 -0
  45. v12x/pylib/scipy/special/add_newdocs.py +15 -0
  46. v12x/pylib/scipy/special/basic.py +84 -0
  47. v12x/pylib/scipy/special/orthogonal.py +45 -0
  48. v12x/pylib/scipy/special/sf_error.py +20 -0
  49. v12x/pylib/scipy/special/specfun.py +21 -0
  50. v12x/pylib/scipy/special/spfun_stats.py +17 -0
v12x/pylib/scipy-1.18.1.dist-info/sboms/auditwheel.cdx.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"bomFormat": "CycloneDX", "specVersion": "1.4", "version": 1, "metadata": {"component": {"type": "library", "bom-ref": "pkg:pypi/scipy@1.18.1?file_name=scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", "name": "scipy", "version": "1.18.1", "purl": "pkg:pypi/scipy@1.18.1?file_name=scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl"}, "tools": [{"name": "auditwheel", "version": "6.7.0"}]}, "components": [{"type": "library", "bom-ref": "pkg:pypi/scipy@1.18.1?file_name=scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", "name": "scipy", "version": "1.18.1", "purl": "pkg:pypi/scipy@1.18.1?file_name=scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl"}, {"type": "library", "bom-ref": "pkg:rpm/almalinux/libgfortran@8.5.0-28.el8_10.alma.1#3362720ce454cc402813e3cfe54f0eb4efaedcd690fa35129ddc4b3de40e0650", "name": "libgfortran", "version": "8.5.0-28.el8_10.alma.1", "purl": "pkg:rpm/almalinux/libgfortran@8.5.0-28.el8_10.alma.1"}, {"type": "library", "bom-ref": "pkg:rpm/almalinux/libquadmath@8.5.0-28.el8_10.alma.1#97b91febe2a1c7927c6482d6bd6567b83be3467f33c9d2764df1283f7ac9a5fa", "name": "libquadmath", "version": "8.5.0-28.el8_10.alma.1", "purl": "pkg:rpm/almalinux/libquadmath@8.5.0-28.el8_10.alma.1"}], "dependencies": [{"ref": "pkg:pypi/scipy@1.18.1?file_name=scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", "dependsOn": ["pkg:rpm/almalinux/libgfortran@8.5.0-28.el8_10.alma.1#3362720ce454cc402813e3cfe54f0eb4efaedcd690fa35129ddc4b3de40e0650", "pkg:rpm/almalinux/libquadmath@8.5.0-28.el8_10.alma.1#97b91febe2a1c7927c6482d6bd6567b83be3467f33c9d2764df1283f7ac9a5fa"]}, {"ref": "pkg:rpm/almalinux/libgfortran@8.5.0-28.el8_10.alma.1#3362720ce454cc402813e3cfe54f0eb4efaedcd690fa35129ddc4b3de40e0650"}, {"ref": "pkg:rpm/almalinux/libquadmath@8.5.0-28.el8_10.alma.1#97b91febe2a1c7927c6482d6bd6567b83be3467f33c9d2764df1283f7ac9a5fa"}]}
v12x/pylib/scipy/spatial/transform/__init__.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Spatial Transformations (:mod:`scipy.spatial.transform`)
3
+ ========================================================
4
+
5
+ .. currentmodule:: scipy.spatial.transform
6
+
7
+ This package implements various spatial transformations. For now, rotations
8
+ and rigid transforms (rotations and translations) are supported.
9
+
10
+ Rotations in 3 dimensions
11
+ -------------------------
12
+ .. autosummary::
13
+ :toctree: generated/
14
+
15
+ RigidTransform
16
+ Rotation
17
+ Slerp
18
+ RotationSpline
19
+ """
20
+ from ._rotation import Rotation, Slerp
21
+ from ._rigid_transform import RigidTransform
22
+ from ._rotation_spline import RotationSpline
23
+
24
+ # Deprecated namespaces, to be removed in v2.0.0
25
+ from . import rotation
26
+
27
+ __all__ = ['Rotation', 'Slerp', 'RotationSpline', 'RigidTransform']
28
+
29
+ from scipy._lib._testutils import PytestTester
30
+ test = PytestTester(__name__)
31
+ del PytestTester
v12x/pylib/scipy/spatial/transform/_rigid_transform.py ADDED
@@ -0,0 +1,1975 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Iterator
4
+ from types import EllipsisType, GenericAlias, ModuleType, NotImplementedType
5
+ from collections.abc import Callable
6
+
7
+ import numpy as np
8
+
9
+ from scipy._lib._array_api import (
10
+ array_namespace,
11
+ is_numpy,
12
+ ArrayLike,
13
+ Array,
14
+ xp_capabilities,
15
+ )
16
+ from scipy.spatial.transform import Rotation
17
+ from scipy.spatial.transform._rotation import _promote
18
+ import scipy.spatial.transform._rigid_transform_cy as cython_backend
19
+ import scipy.spatial.transform._rigid_transform_xp as xp_backend
20
+ import scipy._external.array_api_extra as xpx
21
+ from scipy._external.array_api_compat import device
22
+ from scipy._lib._array_api import xp_promote
23
+ from scipy._lib._util import broadcastable
24
+
25
+
26
+ __all__ = ["RigidTransform"]
27
+
28
+ backend_registry = {array_namespace(np.empty(0)): cython_backend}
29
+
30
+
31
+ def select_backend(xp: ModuleType, cython_compatible: bool):
32
+ """Select the backend for the given array library.
33
+
34
+ We need this selection function because the Cython backend for numpy does not
35
+ support quaternions of arbitrary dimensions. We therefore only use the Array API
36
+ backend for numpy if we are dealing with rotations of more than one leading
37
+ dimension.
38
+ """
39
+ if is_numpy(xp) and not cython_compatible:
40
+ return xp_backend
41
+ return backend_registry.get(xp, xp_backend)
42
+
43
+
44
+ @xp_capabilities()
45
+ def normalize_dual_quaternion(dual_quat: ArrayLike) -> Array:
46
+ """Normalize dual quaternion."""
47
+ xp = array_namespace(dual_quat)
48
+ dual_quat = _promote(dual_quat, xp=xp)
49
+ single = dual_quat.ndim == 1 and is_numpy(xp)
50
+ if single:
51
+ dual_quat = xpx.atleast_nd(dual_quat, ndim=2, xp=xp)
52
+ cython_compatible = dual_quat.ndim < 3
53
+ dq = select_backend(xp, cython_compatible).normalize_dual_quaternion(dual_quat)
54
+ if single:
55
+ return dq[0]
56
+ return dq
57
+
58
+
59
+ rigid_transform_extra_note = (
60
+ """The method ``apply`` is not supported with cupy<14.*.
61
+
62
+ """)
63
+
64
+
65
+ @xp_capabilities(
66
+ skip_backends=[("dask.array", "missing linalg.cross/det functions")],
67
+ method_capabilities={
68
+ "__init__": dict(
69
+ jax_jit=False,
70
+ skip_backends=[("dask.array", "missing linalg.cross/det functions")],
71
+ ),
72
+ "from_matrix": dict(
73
+ jax_jit=False,
74
+ skip_backends=[("dask.array", "missing linalg.cross/det functions")],
75
+ ),
76
+ "__getitem__": dict(
77
+ jax_jit=False,
78
+ skip_backends=[
79
+ ("dask.array", "cannot handle zero-length rigid transforms")
80
+ ],
81
+ ),
82
+ "__setitem__": dict(
83
+ jax_jit=False,
84
+ skip_backends=[
85
+ ("dask.array", "cannot handle zero-length rigid transforms")
86
+ ],
87
+ ),
88
+ "apply": dict(
89
+ skip_backends=[
90
+ ("dask.array", "missing linalg.cross/det functions"),
91
+ ("cupy", "missing .mT attribute in cupy<14.*"),
92
+ ],
93
+ ),
94
+ },
95
+ extra_note=rigid_transform_extra_note,
96
+ )
97
+ class RigidTransform:
98
+ """
99
+ Rigid transform in 3 dimensions.
100
+
101
+ This class provides an interface to initialize from and represent rigid
102
+ transforms (rotation and translation) in 3D space. In different fields,
103
+ this type of transform may be referred to as "*pose*" (especially in
104
+ robotics), "*extrinsic parameters*", or the "*model matrix*" (especially in
105
+ computer graphics), but the core concept is the same: a rotation and
106
+ translation describing the orientation of one 3D coordinate frame relative
107
+ to another. Mathematically, these transforms belong to the Special
108
+ Euclidean group SE(3), which encodes rotation (SO(3)) plus translation.
109
+
110
+ The following operations on rigid transforms are supported:
111
+
112
+ - Application on vectors
113
+ - Transformation composition
114
+ - Transformation inversion
115
+ - Transformation indexing
116
+
117
+ Note that coordinate systems must be right-handed. Because of this, this
118
+ class more precisely represents *proper* rigid transformations in SE(3)
119
+ rather than rigid transforms in E(3) more generally [1]_.
120
+
121
+ Indexing within a transform is supported since multiple transforms can be
122
+ stored within a single `RigidTransform` instance.
123
+
124
+ Multiple transforms can be stored in a single `RigidTransform` object, which can be
125
+ initialized using N-dimensional arrays and supports broadcasting for all
126
+ operations.
127
+
128
+ To create `RigidTransform` objects use ``from_...`` methods (see examples
129
+ below). ``RigidTransform(...)`` is not supposed to be instantiated directly.
130
+
131
+ For rigorous introductions to rigid transforms, see [2]_, [3]_, and [4]_.
132
+
133
+ Parameters
134
+ ----------
135
+ matrix : array_like, shape (..., 4, 4)
136
+ A single transformation matrix or a stack of transformation
137
+ matrices.
138
+ normalize : bool, optional
139
+ If True, orthonormalize the rotation matrix using singular value
140
+ decomposition. If False, the rotation matrix is not checked for
141
+ orthogonality or right-handedness.
142
+ copy : bool, optional
143
+ If True, copy the input matrix. If False, a reference to the input
144
+ matrix is used. If normalize is True, the input matrix is always
145
+ copied regardless of the value of copy.
146
+
147
+ Attributes
148
+ ----------
149
+ single
150
+ rotation
151
+ translation
152
+
153
+ Methods
154
+ -------
155
+ __len__
156
+ __getitem__
157
+ __mul__
158
+ __pow__
159
+ from_matrix
160
+ from_rotation
161
+ from_translation
162
+ from_components
163
+ from_exp_coords
164
+ from_dual_quat
165
+ as_matrix
166
+ as_components
167
+ as_exp_coords
168
+ as_dual_quat
169
+ concatenate
170
+ mean
171
+ apply
172
+ inv
173
+ identity
174
+
175
+ Notes
176
+ -----
177
+ .. versionadded:: 1.16.0
178
+
179
+ References
180
+ ----------
181
+ .. [1] https://en.wikipedia.org/wiki/Rigid_transformation
182
+ .. [2] https://motion.cs.illinois.edu/RoboticSystems/CoordinateTransformations.html
183
+ .. [3] https://www.brainvoyager.com/bv/doc/UsersGuide/CoordsAndTransforms/SpatialTransformationMatrices.html
184
+ .. [4] Kevin M. Lynch and Frank C. Park, "Modern Robotics: Mechanics,
185
+ Planning, and Control" Chapter 3.3, 2017, Cambridge University Press.
186
+ https://hades.mech.northwestern.edu/images/2/25/MR-v2.pdf#page=107.31
187
+ .. [5] Paul Furgale, "Representing Robot Pose: The good, the bad, and the
188
+ ugly", June 9, 2014.
189
+ https://rpg.ifi.uzh.ch/docs/teaching/2024/FurgaleTutorial.pdf
190
+
191
+ Examples
192
+ --------
193
+ A `RigidTransform` instance can be initialized in any of the above formats
194
+ and converted to any of the others. The underlying object is independent of
195
+ the representation used for initialization.
196
+
197
+ **Notation Conventions and Composition**
198
+
199
+ The notation here largely follows the convention defined in [5]_. When we
200
+ name transforms, we read the subscripts from right to left. So ``tf_A_B``
201
+ represents a transform A <- B and can be interpreted as:
202
+
203
+ - the coordinates and orientation of B relative to A
204
+ - the transformation of points from B to A
205
+ - the pose of B described in A's coordinate system
206
+
207
+ .. parsed-literal::
208
+ :class: highlight-none
209
+
210
+ tf_A_B
211
+ ^ ^
212
+ | |
213
+ | --- from B
214
+ |
215
+ ----- to A
216
+
217
+ When composing transforms, the order is important. Transforms are not
218
+ commutative, so in general ``tf_A_B * tf_B_C`` is not the same as
219
+ ``tf_B_C * tf_A_B``. Transforms are composed and applied to vectors
220
+ right-to-left. So ``(tf_A_B * tf_B_C).apply(p_C)`` is the same as
221
+ ``tf_A_B.apply(tf_B_C.apply(p_C))``.
222
+
223
+ When composed, transforms should be ordered such that the multiplication
224
+ operator is surrounded by a single frame, so the frame "cancels out" and
225
+ the outside frames are left. In the example below, B cancels out and the
226
+ outside frames A and C are left. Or to put it another way, A <- C is the
227
+ same as A <- B <- C.
228
+
229
+ .. parsed-literal::
230
+ :class: highlight-none
231
+
232
+ ----------- B cancels out
233
+ | |
234
+ v v
235
+ tf_A_C = tf_A_B * tf_B_C
236
+ ^ ^
237
+ | |
238
+ ------------ to A, from C are left
239
+
240
+ When we notate vectors, we write the subscript of the frame that the vector
241
+ is defined in. So ``p_B`` means the point ``p`` defined in frame B. To
242
+ transform this point from frame B to coordinates in frame A, we apply the
243
+ transform ``tf_A_B`` to the vector, lining things up such that the notated
244
+ B frames are next to each other and "cancel out".
245
+
246
+ .. parsed-literal::
247
+ :class: highlight-none
248
+
249
+ ------------ B cancels out
250
+ | |
251
+ v v
252
+ p_A = tf_A_B.apply(p_B)
253
+ ^
254
+ |
255
+ -------------- A is left
256
+
257
+ **Visualization**
258
+
259
+ >>> from scipy.spatial.transform import RigidTransform as Tf
260
+ >>> from scipy.spatial.transform import Rotation as R
261
+ >>> import numpy as np
262
+
263
+ The following function can be used to plot transforms with Matplotlib
264
+ by showing how they transform the standard x, y, z coordinate axes:
265
+
266
+ >>> import matplotlib.pyplot as plt
267
+ >>> colors = ("#FF6666", "#005533", "#1199EE") # Colorblind-safe RGB
268
+ >>> def plot_transformed_axes(ax, tf, name=None, scale=1):
269
+ ... r = tf.rotation
270
+ ... t = tf.translation
271
+ ... loc = np.array([t, t])
272
+ ... for i, (axis, c) in enumerate(zip((ax.xaxis, ax.yaxis, ax.zaxis),
273
+ ... colors)):
274
+ ... axlabel = axis.axis_name
275
+ ... axis.set_label_text(axlabel)
276
+ ... axis.label.set_color(c)
277
+ ... axis.line.set_color(c)
278
+ ... axis.set_tick_params(colors=c)
279
+ ... line = np.zeros((2, 3))
280
+ ... line[1, i] = scale
281
+ ... line_rot = r.apply(line)
282
+ ... line_plot = line_rot + loc
283
+ ... ax.plot(line_plot[:, 0], line_plot[:, 1], line_plot[:, 2], c)
284
+ ... text_loc = line[1]*1.2
285
+ ... text_loc_rot = r.apply(text_loc)
286
+ ... text_plot = text_loc_rot + t
287
+ ... ax.text(*text_plot, axlabel.upper(), color=c,
288
+ ... va="center", ha="center")
289
+ ... ax.text(*tf.translation, name, color="k", va="center", ha="center",
290
+ ... bbox={"fc": "w", "alpha": 0.8, "boxstyle": "circle"})
291
+
292
+ **Defining Frames**
293
+
294
+ Let's work through an example.
295
+
296
+ First, define the "world frame" A, also called the "base frame".
297
+ All frames are the identity transform from their own perspective.
298
+
299
+ >>> tf_A = Tf.identity()
300
+
301
+ We will visualize a new frame B in A's coordinate system. So we need to
302
+ define the transform that converts coordinates from frame B to frame A
303
+ (A <- B).
304
+
305
+ Physically, let's imagine constructing B from A by:
306
+
307
+ 1) Rotating A by +90 degrees around its x-axis.
308
+ 2) Translating the rotated frame 2 units in A's -x direction.
309
+
310
+ From A's perspective, B is at [-2, 0, 0] and rotated +90 degrees about the
311
+ x-axis, which is exactly the transform A <- B.
312
+
313
+ >>> t_A_B = np.array([-2, 0, 0])
314
+ >>> r_A_B = R.from_euler('xyz', [90, 0, 0], degrees=True)
315
+ >>> tf_A_B = Tf.from_components(t_A_B, r_A_B)
316
+
317
+ Let's plot these frames.
318
+
319
+ >>> fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
320
+ >>> plot_transformed_axes(ax, tf_A, name="tfA") # A plotted in A
321
+ >>> plot_transformed_axes(ax, tf_A_B, name="tfAB") # B plotted in A
322
+ >>> ax.set_title("A, B frames with respect to A")
323
+ >>> ax.set_aspect("equal")
324
+ >>> ax.figure.set_size_inches(6, 5)
325
+ >>> plt.show()
326
+
327
+ Now let's visualize a new frame C in B's coordinate system.
328
+ Let's imagine constructing C from B by:
329
+
330
+ 1) Translating B by 2 units in its +z direction.
331
+ 2) Rotating B by +30 degrees around its z-axis.
332
+
333
+ >>> t_B_C = np.array([0, 0, 2])
334
+ >>> r_B_C = R.from_euler('xyz', [0, 0, 30], degrees=True)
335
+ >>> tf_B_C = Tf.from_components(t_B_C, r_B_C)
336
+
337
+ In order to plot these frames from a consistent perspective, we need to
338
+ calculate the transform between A and C. Note that we do not make this
339
+ transform directly, but instead compose intermediate transforms that let us
340
+ get from C to A:
341
+
342
+ >>> tf_A_C = tf_A_B * tf_B_C # A <- B <- C
343
+
344
+ Now we can plot these three frames from A's perspective.
345
+
346
+ >>> fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
347
+ >>> plot_transformed_axes(ax, tf_A, name="tfA") # A plotted in A
348
+ >>> plot_transformed_axes(ax, tf_A_B, name="tfAB") # B plotted in A
349
+ >>> plot_transformed_axes(ax, tf_A_C, name="tfAC") # C plotted in A
350
+ >>> ax.set_title("A, B, C frames with respect to A")
351
+ >>> ax.set_aspect("equal")
352
+ >>> ax.figure.set_size_inches(6, 5)
353
+ >>> plt.show()
354
+
355
+ **Transforming Vectors**
356
+
357
+ Let's transform a vector from A, to B and C. To do this, we will first
358
+ invert the transforms we already have from B and C, to A.
359
+
360
+ >>> tf_B_A = tf_A_B.inv() # B <- A
361
+ >>> tf_C_A = tf_A_C.inv() # C <- A
362
+
363
+ Now we can define a point in A and use the above transforms to get its
364
+ coordinates in B and C:
365
+
366
+ >>> p1_A = np.array([1, 0, 0]) # +1 in x_A direction
367
+ >>> p1_B = tf_B_A.apply(p1_A)
368
+ >>> p1_C = tf_C_A.apply(p1_A)
369
+ >>> print(p1_A) # Original point 1 in A
370
+ [1 0 0]
371
+ >>> print(p1_B) # Point 1 in B
372
+ [3. 0. 0.]
373
+ >>> print(p1_C) # Point 1 in C
374
+ [ 2.59807621 -1.5 -2. ]
375
+
376
+ We can also do the reverse. We define a point in C and transform it to A:
377
+
378
+ >>> p2_C = np.array([0, 1, 0]) # +1 in y_C direction
379
+ >>> p2_A = tf_A_C.apply(p2_C)
380
+ >>> print(p2_C) # Original point 2 in C
381
+ [0 1 0]
382
+ >>> print(p2_A) # Point 2 in A
383
+ [-2.5 -2. 0.8660254]
384
+
385
+ Plot the frames with respect to A again, but also plot these two points:
386
+
387
+ >>> fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
388
+ >>> plot_transformed_axes(ax, tf_A, name="tfA") # A plotted in A
389
+ >>> plot_transformed_axes(ax, tf_A_B, name="tfAB") # B plotted in A
390
+ >>> plot_transformed_axes(ax, tf_A_C, name="tfAC") # C plotted in A
391
+ >>> ax.scatter(p1_A[0], p1_A[1], p1_A[2], color=colors[0]) # +1 x_A
392
+ >>> ax.scatter(p2_A[0], p2_A[1], p2_A[2], color=colors[1]) # +1 y_C
393
+ >>> ax.set_title("A, B, C frames and points with respect to A")
394
+ >>> ax.set_aspect("equal")
395
+ >>> ax.figure.set_size_inches(6, 5)
396
+ >>> plt.show()
397
+
398
+ **Switching Base Frames**
399
+
400
+ Up to this point, we have been visualizing frames from A's perspective.
401
+ Let's use the transforms we defined to visualize the frames from C's
402
+ perspective.
403
+
404
+ Now C is the "base frame" or "world frame". All frames are the identity
405
+ transform from their own perspective.
406
+
407
+ >>> tf_C = Tf.identity()
408
+
409
+ We've already defined the transform C <- A, and can obtain C <- B by
410
+ inverting the existing transform B <- C.
411
+
412
+ >>> tf_C_B = tf_B_C.inv() # C <- B
413
+
414
+ This lets us plot everything from C's perspective:
415
+
416
+ >>> fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
417
+ >>> plot_transformed_axes(ax, tf_C, name="tfC") # C plotted in C
418
+ >>> plot_transformed_axes(ax, tf_C_B, name="tfCB") # B plotted in C
419
+ >>> plot_transformed_axes(ax, tf_C_A, name="tfCA") # A plotted in C
420
+ >>> ax.scatter(p1_C[0], p1_C[1], p1_C[2], color=colors[0])
421
+ >>> ax.scatter(p2_C[0], p2_C[1], p2_C[2], color=colors[1])
422
+ >>> ax.set_title("A, B, C frames and points with respect to C")
423
+ >>> ax.set_aspect("equal")
424
+ >>> ax.figure.set_size_inches(6, 5)
425
+ >>> plt.show()
426
+ """
427
+
428
+ # generic type compatibility with scipy-stubs
429
+ __class_getitem__: classmethod = classmethod(GenericAlias)
430
+
431
+ def __init__(self, matrix: ArrayLike, normalize: bool = True, copy: bool = True):
432
+ """Initialize from a 4x4 transformation matrix.
433
+
434
+ Rotations are not meant to be initialized directly. Please use one of
435
+ the `from_...` methods instead.
436
+
437
+ Parameters
438
+ ----------
439
+ matrix : array_like, shape (..., 4, 4)
440
+ A single transformation matrix or a stack of transformation
441
+ matrices.
442
+ normalize : bool, optional
443
+ If True, orthonormalize the rotation matrix using singular value
444
+ decomposition. If False, the rotation matrix is not checked for
445
+ orthogonality or right-handedness.
446
+ copy : bool, optional
447
+ If True, copy the input matrix. If False, a reference to the input
448
+ matrix is used. If normalize is True, the input matrix is always
449
+ copied regardless of the value of copy.
450
+
451
+ Returns
452
+ -------
453
+ transform : `RigidTransform` instance
454
+ """
455
+ xp = array_namespace(matrix)
456
+ self._xp = xp
457
+ matrix = _promote(matrix, xp=xp)
458
+ if matrix.shape[-2:] != (4, 4):
459
+ raise ValueError(
460
+ f"Expected `matrix` to have shape (..., 4, 4), got {matrix.shape}."
461
+ )
462
+ # We only need the _single flag for the cython backend. The Array API backend
463
+ # uses broadcasting by default and hence returns the correct shape without
464
+ # additional _single logic
465
+ self._single = matrix.ndim == 2 and is_numpy(xp)
466
+ if self._single:
467
+ matrix = xpx.atleast_nd(matrix, ndim=3, xp=xp)
468
+
469
+ self._backend = select_backend(xp, matrix.ndim < 4)
470
+ self._matrix = self._backend.from_matrix(matrix, normalize, copy)
471
+
472
+ def __repr__(self):
473
+ m = f"{self.as_matrix()!r}".splitlines()
474
+ # bump indent (+27 characters)
475
+ m[1:] = [" " * 27 + m[i] for i in range(1, len(m))]
476
+ return "RigidTransform.from_matrix(" + "\n".join(m) + ")"
477
+
478
+ @staticmethod
479
+ def from_matrix(matrix: ArrayLike) -> RigidTransform:
480
+ """Initialize from a 4x4 transformation matrix.
481
+
482
+ Parameters
483
+ ----------
484
+ matrix : array_like, shape (..., 4, 4)
485
+ Transformation matrices. Each matrix[..., :, :] represents a 4x4
486
+ transformation matrix.
487
+
488
+ Returns
489
+ -------
490
+ transform : `RigidTransform` instance
491
+ Object containing the rigid transform represented by the input matrix.
492
+
493
+ Notes
494
+ -----
495
+ 4x4 rigid transformation matrices are of the form::
496
+
497
+ [ tx]
498
+ [ R ty]
499
+ [ tz]
500
+ [ 0 0 0 1]
501
+
502
+ where ``R`` is a 3x3 rotation matrix and ``t = [tx, ty, tz]`` is a 3x1
503
+ translation vector. As rotation matrices must be proper orthogonal, the
504
+ rotation component is orthonormalized using singular value decomposition
505
+ before initialization.
506
+
507
+ Examples
508
+ --------
509
+ >>> from scipy.spatial.transform import RigidTransform as Tf
510
+ >>> import numpy as np
511
+
512
+ Creating a transform from a single matrix:
513
+
514
+ >>> m = np.array([[0, 1, 0, 2],
515
+ ... [0, 0, 1, 3],
516
+ ... [1, 0, 0, 4],
517
+ ... [0, 0, 0, 1]])
518
+ >>> tf = Tf.from_matrix(m)
519
+ >>> tf.as_matrix()
520
+ array([[0., 1., 0., 2.],
521
+ [0., 0., 1., 3.],
522
+ [1., 0., 0., 4.],
523
+ [0., 0., 0., 1.]])
524
+ >>> tf.single
525
+ True
526
+
527
+ Creating a transform from an N-dimensional array of matrices:
528
+
529
+ >>> m = np.tile(np.eye(4), (2, 5, 1, 1)) # Shape (2, 5, 4, 4)
530
+ >>> tf = Tf.from_matrix(m)
531
+ >>> tf.shape
532
+ (2, 5)
533
+ >>> tf.single
534
+ False
535
+ >>> len(tf)
536
+ 2
537
+
538
+ Matrices with a rotation component that is not proper orthogonal are
539
+ orthogonalized using singular value decomposition before initialization:
540
+
541
+ >>> tf = Tf.from_matrix(np.diag([2, 2, 2, 1]))
542
+ >>> tf.as_matrix()
543
+ array([[1., 0., 0., 0.],
544
+ [0., 1., 0., 0.],
545
+ [0., 0., 1., 0.],
546
+ [0., 0., 0., 1.]])
547
+ """
548
+ return RigidTransform(matrix, normalize=True, copy=True)
549
+
550
+ @staticmethod
551
+ def from_rotation(rotation: Rotation) -> RigidTransform:
552
+ """Initialize from a rotation, without a translation.
553
+
554
+ When applying this transform to a vector ``v``, the result is the
555
+ same as if the rotation was applied to the vector.
556
+ ``Tf.from_rotation(r).apply(v) == r.apply(v)``
557
+
558
+ Parameters
559
+ ----------
560
+ rotation : `Rotation` instance
561
+ A single rotation or a rotation with N leading dimensions.
562
+
563
+ Returns
564
+ -------
565
+ transform : `RigidTransform` instance
566
+ Object containing the rigid transform represented by the input rotation.
567
+
568
+ Examples
569
+ --------
570
+ >>> from scipy.spatial.transform import RigidTransform as Tf
571
+ >>> from scipy.spatial.transform import Rotation as R
572
+ >>> import numpy as np
573
+
574
+ Creating a transform from a single rotation:
575
+
576
+ >>> r = R.from_euler("ZYX", [90, 30, 0], degrees=True)
577
+ >>> r.apply([1, 0, 0])
578
+ array([0. , 0.8660254, -0.5 ])
579
+ >>> tf = Tf.from_rotation(r)
580
+ >>> tf.apply([1, 0, 0])
581
+ array([0. , 0.8660254, -0.5 ])
582
+ >>> tf.single
583
+ True
584
+
585
+ The upper 3x3 submatrix of the transformation matrix is the rotation
586
+ matrix:
587
+
588
+ >>> np.allclose(tf.as_matrix()[:3, :3], r.as_matrix(), atol=1e-12)
589
+ True
590
+
591
+ Creating multiple transforms from a rotation with N leading dimensions:
592
+
593
+ >>> r = R.from_euler("ZYX", [[90, 30, 0], [45, 30, 60]], degrees=True)
594
+ >>> r.apply([1, 0, 0])
595
+ array([[0. , 0.8660254 , -0.5 ],
596
+ [0.61237244, 0.61237244, -0.5 ]])
597
+ >>> tf = Tf.from_rotation(r)
598
+ >>> tf.apply([1, 0, 0])
599
+ array([[0. , 0.8660254 , -0.5 ],
600
+ [0.61237244, 0.61237244, -0.5 ]])
601
+ >>> tf.single
602
+ False
603
+ >>> len(tf)
604
+ 2
605
+ """
606
+ if not isinstance(rotation, Rotation):
607
+ raise TypeError(
608
+ "Expected `rotation` to be a `Rotation` instance, "
609
+ f"got {type(rotation)}."
610
+ )
611
+ quat = rotation.as_quat()
612
+ xp = array_namespace(quat)
613
+ backend = select_backend(xp, quat.ndim < 3)
614
+ matrix = backend.from_rotation(quat)
615
+ return RigidTransform._from_raw_matrix(matrix, xp, backend)
616
+
617
+ @staticmethod
618
+ def from_translation(translation: ArrayLike) -> RigidTransform:
619
+ """Initialize from a translation numpy array, without a rotation.
620
+
621
+ When applying this transform to a vector ``v``, the result is the same
622
+ as if the translation and vector were added together. If ``t`` is the
623
+ displacement vector of the translation, then:
624
+
625
+ ``Tf.from_translation(t).apply(v) == t + v``
626
+
627
+ Parameters
628
+ ----------
629
+ translation : array_like, shape (..., 3)
630
+ Translation vectors. Each translation[..., :] represents a 3D
631
+ translation vector.
632
+
633
+ Returns
634
+ -------
635
+ transform : `RigidTransform` instance
636
+ Object containing the rigid transform represented by the input translation.
637
+
638
+ Examples
639
+ --------
640
+ >>> from scipy.spatial.transform import RigidTransform as Tf
641
+ >>> import numpy as np
642
+
643
+ Creating a transform from a single translation vector:
644
+
645
+ >>> t = np.array([2, 3, 4])
646
+ >>> t + np.array([1, 0, 0])
647
+ array([3, 3, 4])
648
+ >>> tf = Tf.from_translation(t)
649
+ >>> tf.apply([1, 0, 0])
650
+ array([3., 3., 4.])
651
+ >>> tf.single
652
+ True
653
+
654
+ The top 3x1 points in the rightmost column of the transformation matrix
655
+ is the translation vector:
656
+
657
+ >>> tf.as_matrix()
658
+ array([[1., 0., 0., 2.],
659
+ [0., 1., 0., 3.],
660
+ [0., 0., 1., 4.],
661
+ [0., 0., 0., 1.]])
662
+ >>> np.allclose(tf.as_matrix()[:3, 3], t)
663
+ True
664
+
665
+ Creating multiple transforms from an N-dimensional array of translation
666
+ vectors:
667
+
668
+ >>> t = np.array([[2, 3, 4], [1, 0, 0]])
669
+ >>> t + np.array([1, 0, 0])
670
+ array([[3, 3, 4],
671
+ [2, 0, 0]])
672
+ >>> tf = Tf.from_translation(t)
673
+ >>> tf.apply([1, 0, 0])
674
+ array([[3., 3., 4.],
675
+ [2., 0., 0.]])
676
+ >>> np.allclose(tf.as_matrix()[:, :3, 3], t)
677
+ True
678
+ >>> tf.single
679
+ False
680
+ >>> len(tf)
681
+ 2
682
+ """
683
+ xp = array_namespace(translation)
684
+ translation = _promote(translation, xp=xp)
685
+ backend = select_backend(xp, translation.ndim < 3)
686
+ matrix = backend.from_translation(translation)
687
+ return RigidTransform._from_raw_matrix(matrix, xp, backend)
688
+
689
+ @staticmethod
690
+ def from_components(translation: ArrayLike, rotation: Rotation) -> RigidTransform:
691
+ """Initialize a rigid transform from translation and rotation
692
+ components.
693
+
694
+ When creating a rigid transform from a translation and rotation, the
695
+ translation is applied after the rotation, such that
696
+ ``tf = Tf.from_components(translation, rotation)``
697
+ is equivalent to
698
+ ``tf = Tf.from_translation(translation) * Tf.from_rotation(rotation)``.
699
+
700
+ When applying a transform to a vector ``v``, the result is the
701
+ same as if the transform was applied to the vector in the
702
+ following way: ``tf.apply(v) == translation + rotation.apply(v)``
703
+
704
+ Parameters
705
+ ----------
706
+ translation : array_like, shape (..., 3)
707
+ Translation vectors. Each translation[..., :] represents a 3D
708
+ translation vector.
709
+ rotation : `Rotation` instance
710
+ Rotation objects. The shape must be broadcastable with the
711
+ translation shape.
712
+
713
+ Returns
714
+ -------
715
+ transform : `RigidTransform` instance
716
+ Rigid transform objects. The shape is determined by broadcasting
717
+ the translation and rotation shapes together.
718
+
719
+ Examples
720
+ --------
721
+ >>> from scipy.spatial.transform import RigidTransform as Tf
722
+ >>> from scipy.spatial.transform import Rotation as R
723
+ >>> import numpy as np
724
+
725
+ Creating from a single rotation and translation:
726
+
727
+ >>> t = np.array([2, 3, 4])
728
+ >>> r = R.from_euler("ZYX", [90, 30, 0], degrees=True)
729
+ >>> r.as_matrix()
730
+ array([[ 0. , -1., 0. ],
731
+ [ 0.8660254, 0., 0.5 ],
732
+ [-0.5 , 0., 0.8660254 ]])
733
+ >>> tf = Tf.from_components(t, r)
734
+ >>> tf.rotation.as_matrix()
735
+ array([[ 0. , -1., 0. ],
736
+ [ 0.8660254, 0., 0.5 ],
737
+ [-0.5 , 0., 0.8660254 ]])
738
+ >>> tf.translation
739
+ array([2., 3., 4.])
740
+ >>> tf.single
741
+ True
742
+
743
+ When applying a transform to a vector ``v``, the result is the same as
744
+ if the transform was applied to the vector in the following way:
745
+ ``tf.apply(v) == translation + rotation.apply(v)``
746
+
747
+ >>> r.apply([1, 0, 0])
748
+ array([0. , 0.8660254, -0.5 ])
749
+ >>> t + r.apply([1, 0, 0])
750
+ array([2. , 3.8660254, 3.5 ])
751
+ >>> tf.apply([1, 0, 0])
752
+ array([2. , 3.8660254, 3.5 ])
753
+ """
754
+ rotation_tf = RigidTransform.from_rotation(rotation)
755
+ return RigidTransform.from_translation(translation) * rotation_tf
756
+
757
+ @staticmethod
758
+ def from_exp_coords(exp_coords: ArrayLike) -> RigidTransform:
759
+ r"""Initialize from exponential coordinates of transform.
760
+
761
+ This implements the exponential map that converts 6-dimensional real
762
+ vectors to SE(3).
763
+
764
+ An exponential coordinate vector consists of 6 elements
765
+ ``[rx, ry, rz, vx, vy, vz]``. The first 3 encode rotation (and form a
766
+ rotation vector used in `Rotation.from_rotvec`) and the last 3 encode
767
+ translation (and form a translation vector for pure translations).
768
+ The exponential mapping can be expressed as matrix exponential
769
+ ``T = exp(tau)``, where ``T`` is a 4x4 matrix representing a rigid
770
+ transform and ``tau`` is a 4x4 matrix formed from the elements of an
771
+ exponential coordinate vector::
772
+
773
+ tau = [ 0 -rz ry vx]
774
+ [ rz 0 -rx vy]
775
+ [-ry rx 0 vz]
776
+ [ 0 0 0 1]
777
+
778
+ Parameters
779
+ ----------
780
+ exp_coords : array_like, shape (..., 6)
781
+ Exponential coordinate vectors. Each exp_coords[..., :] represents
782
+ a 6D exponential coordinate vector with the expected order of
783
+ components ``[rx, ry, rz, vx, vy, vz]``. The first 3 components
784
+ encode rotation and the last 3 encode translation.
785
+
786
+ Returns
787
+ -------
788
+ transform : `RigidTransform` instance
789
+ Rigid transform objects with the same leading dimensions as the input.
790
+
791
+ Examples
792
+ --------
793
+ >>> from scipy.spatial.transform import RigidTransform as Tf
794
+ >>> import numpy as np
795
+
796
+ Creating from a single 6d vector of exponential coordinates:
797
+
798
+ >>> tf = Tf.from_exp_coords([
799
+ ... -2.01041204, -0.52983629, 0.65773501,
800
+ ... 0.10386614, 0.05855009, 0.54959179])
801
+ >>> tf.as_matrix()
802
+ array([[0.76406621, 0.10504613, -0.63652819, -0.10209961],
803
+ [0.59956454, -0.47987325, 0.64050295, 0.40158789],
804
+ [-0.2381705, -0.87102639, -0.42963687, 0.19637636],
805
+ [0., 0., 0., 1.]])
806
+ >>> tf.single
807
+ True
808
+
809
+ A vector of zeros represents the identity transform:
810
+
811
+ >>> tf = Tf.from_exp_coords(np.zeros(6))
812
+ >>> tf.as_matrix()
813
+ array([[1., 0., 0., 0.],
814
+ [0., 1., 0., 0.],
815
+ [0., 0., 1., 0.],
816
+ [0., 0., 0., 1.]])
817
+
818
+ The last three numbers encode translation. If the first three numbers
819
+ are zero, the last three components can be interpreted as the
820
+ translation:
821
+
822
+ >>> tf_trans = Tf.from_exp_coords([0, 0, 0, 4.3, -2, 3.4])
823
+ >>> tf_trans.translation
824
+ array([4.3, -2., 3.4])
825
+
826
+ The first three numbers encode rotation as a rotation vector:
827
+
828
+ >>> tf_rot = Tf.from_exp_coords([0.5, 0.3, 0.1, 0, 0, 0])
829
+ >>> tf_rot.rotation.as_rotvec()
830
+ array([0.5, 0.3, 0.1])
831
+
832
+ Combining translation and rotation preserves the rotation vector,
833
+ but changes the last three components as they encode translation and
834
+ rotation:
835
+
836
+ >>> (tf_trans * tf_rot).as_exp_coords()
837
+ array([0.5, 0.3, 0.1, 3.64305882, -1.25879559, 4.46109265])
838
+ """
839
+ xp = array_namespace(exp_coords)
840
+ exp_coords = xp_promote(exp_coords, force_floating=True, xp=xp)
841
+ if exp_coords.shape[-1] != 6:
842
+ raise ValueError(
843
+ f"Expected `exp_coords` to have shape (..., 6), got {exp_coords.shape}."
844
+ )
845
+ backend = select_backend(xp, cython_compatible=exp_coords.ndim < 3)
846
+ matrix = backend.from_exp_coords(exp_coords)
847
+ return RigidTransform._from_raw_matrix(matrix, xp, backend)
848
+
849
+ @staticmethod
850
+ def from_dual_quat(
851
+ dual_quat: ArrayLike, *, scalar_first: bool = False
852
+ ) -> RigidTransform:
853
+ """Initialize from a unit dual quaternion.
854
+
855
+ Unit dual quaternions encode orientation in a real unit quaternion
856
+ and translation in a dual quaternion. There is a double cover, i.e.,
857
+ the unit dual quaternions q and -q represent the same transform.
858
+
859
+ Unit dual quaternions must have a real quaternion with unit norm and
860
+ a dual quaternion that is orthogonal to the real quaternion to satisfy
861
+ the unit norm constraint. This function will enforce both properties
862
+ through normalization.
863
+
864
+ Parameters
865
+ ----------
866
+ dual_quat : array_like, shape (..., 8)
867
+ Unit dual quaternions. Each dual_quat[..., :] represents a unit
868
+ dual quaternion. The real part is stored in the first four
869
+ components and the dual part in the last four components.
870
+ scalar_first : bool, optional
871
+ Whether the scalar component goes first or last in the two
872
+ individual quaternions that represent the real and the dual part.
873
+ Default is False, i.e. the scalar-last order is used.
874
+
875
+ Returns
876
+ -------
877
+ transform : `RigidTransform` instance
878
+ Rigid transform objects with the same leading dimensions as the input.
879
+
880
+ Examples
881
+ --------
882
+ >>> from scipy.spatial.transform import RigidTransform as Tf
883
+ >>> import numpy as np
884
+
885
+ Creating from a single unit dual quaternion:
886
+
887
+ >>> tf = Tf.from_dual_quat([
888
+ ... 0.0617101, -0.06483886, 0.31432811, 0.94508498,
889
+ ... 0.04985168, -0.26119618, 0.1691491, -0.07743254])
890
+ >>> tf.as_matrix()
891
+ array([[0.79398752, -0.60213598, -0.08376202, 0.24605262],
892
+ [0.58613113, 0.79477941, -0.15740392, -0.4932833],
893
+ [0.16135089, 0.07588122, 0.98397557, 0.34262676],
894
+ [0., 0., 0., 1.]])
895
+ >>> tf.single
896
+ True
897
+ """
898
+ xp = array_namespace(dual_quat)
899
+ dual_quat = _promote(dual_quat, xp=xp)
900
+ backend = select_backend(xp, dual_quat.ndim < 3)
901
+ matrix = backend.from_dual_quat(dual_quat, scalar_first=scalar_first)
902
+ return RigidTransform._from_raw_matrix(matrix, xp, backend)
903
+
904
+ @staticmethod
905
+ def identity(
906
+ num: int | None = None, *, shape: int | tuple[int, ...] | None = None
907
+ ) -> RigidTransform:
908
+ """Initialize an identity transform.
909
+
910
+ Composition with the identity transform has no effect, and
911
+ applying the identity transform to a vector has no effect.
912
+
913
+ Parameters
914
+ ----------
915
+ num : int, optional
916
+ Number of identity transforms to generate. If None (default),
917
+ then a single transform is generated.
918
+ shape : int or tuple of ints, optional
919
+ Shape of the identity transforms. If specified, `num` must
920
+ be None.
921
+
922
+ Returns
923
+ -------
924
+ transform : `RigidTransform` instance
925
+ The identity transform.
926
+
927
+ Examples
928
+ --------
929
+ >>> from scipy.spatial.transform import RigidTransform as Tf
930
+ >>> from scipy.spatial.transform import Rotation as R
931
+ >>> import numpy as np
932
+
933
+ Creating a single identity transform:
934
+
935
+ >>> tf = Tf.identity()
936
+ >>> tf.as_matrix()
937
+ array([[1., 0., 0., 0.],
938
+ [0., 1., 0., 0.],
939
+ [0., 0., 1., 0.],
940
+ [0., 0., 0., 1.]])
941
+ >>> tf.single
942
+ True
943
+
944
+ The identity transform can be applied to a vector without effect:
945
+
946
+ >>> tf.apply([1, 2, 3])
947
+ array([1., 2., 3.])
948
+
949
+ The identity transform when composed with another transform has no
950
+ effect:
951
+
952
+ >>> rng = np.random.default_rng(123)
953
+ >>> t = rng.random(3)
954
+ >>> r = R.random(rng=rng)
955
+ >>> tf = Tf.from_components(t, r)
956
+ >>> np.allclose((Tf.identity() * tf).as_matrix(),
957
+ ... tf.as_matrix(), atol=1e-12)
958
+ True
959
+
960
+ Multiple identity transforms can be generated at once:
961
+
962
+ >>> tf = Tf.identity(2)
963
+ >>> tf.as_matrix()
964
+ array([[[1., 0., 0., 0.],
965
+ [0., 1., 0., 0.],
966
+ [0., 0., 1., 0.],
967
+ [0., 0., 0., 1.]],
968
+ [[1., 0., 0., 0.],
969
+ [0., 1., 0., 0.],
970
+ [0., 0., 1., 0.],
971
+ [0., 0., 0., 1.]]])
972
+ >>> tf.single
973
+ False
974
+ >>> len(tf)
975
+ 2
976
+ """
977
+ if num is not None and shape is not None:
978
+ raise ValueError("Only one of `num` and `shape` can be specified.")
979
+ if num is None and shape is None:
980
+ shape = ()
981
+ elif num is not None:
982
+ shape = (num,)
983
+ elif isinstance(shape, int):
984
+ shape = (shape,)
985
+ elif not isinstance(shape, tuple):
986
+ raise ValueError("`shape` must be an int or a tuple of ints or None.")
987
+ matrix = np.tile(np.eye(4), shape + (1, 1))
988
+ # No need for a backend call here since identity is easy to construct and we are
989
+ # currently not offering a backend-specific identity matrix
990
+ return RigidTransform._from_raw_matrix(matrix, array_namespace(matrix))
991
+
992
+ @staticmethod
993
+ def concatenate(
994
+ transforms: RigidTransform | Iterable[RigidTransform],
995
+ ) -> RigidTransform:
996
+ """
997
+ Concatenate a sequence of `RigidTransform` objects into a
998
+ single object.
999
+
1000
+ Parameters
1001
+ ----------
1002
+ transforms : sequence of `RigidTransform`
1003
+ If a single `RigidTransform` instance is passed in, a copy of
1004
+ it is returned.
1005
+
1006
+ Returns
1007
+ -------
1008
+ transform : `RigidTransform` instance
1009
+ The concatenated transform.
1010
+
1011
+ Examples
1012
+ --------
1013
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1014
+ >>> tf1 = Tf.from_translation([1, 0, 0])
1015
+ >>> tf2 = Tf.from_translation([[2, 0, 0], [3, 0, 0]])
1016
+ >>> Tf.concatenate([tf1, tf2]).translation
1017
+ array([[1., 0., 0.],
1018
+ [2., 0., 0.],
1019
+ [3., 0., 0.]])
1020
+ """
1021
+ if isinstance(transforms, RigidTransform):
1022
+ return RigidTransform._from_raw_matrix(
1023
+ transforms.as_matrix(), transforms._xp
1024
+ )
1025
+ if not all(isinstance(x, RigidTransform) for x in transforms):
1026
+ raise TypeError("input must contain RigidTransform objects only")
1027
+
1028
+ xp = array_namespace(transforms[0].as_matrix()) # type:ignore[index]
1029
+ matrix = xp.concat(
1030
+ [xpx.atleast_nd(x.as_matrix(), ndim=3, xp=xp) for x in transforms]
1031
+ )
1032
+ return RigidTransform._from_raw_matrix(matrix, xp, None)
1033
+
1034
+ def mean(self,
1035
+ weights: ArrayLike | None = None,
1036
+ axis: None | int | tuple[int, ...] = None
1037
+ ) -> RigidTransform:
1038
+ """Get the mean of the transforms.
1039
+
1040
+ The mean of a set of transforms is the same as the mean of its
1041
+ rotation and translation components.
1042
+
1043
+ The mean used for the rotation component is the chordal L2 mean (also
1044
+ called the projected or induced arithmetic mean) [1]_. If ``A`` is a
1045
+ set of rotation matrices, then the mean ``M`` is the rotation matrix
1046
+ that minimizes the following loss function:
1047
+
1048
+ .. math::
1049
+
1050
+ L(M) = \\sum_{i = 1}^{n} w_i \\lVert \\mathbf{A}_i -
1051
+ \\mathbf{M} \\rVert^2 ,
1052
+
1053
+ where :math:`w_i`'s are the `weights` corresponding to each matrix.
1054
+
1055
+ Parameters
1056
+ ----------
1057
+ weights : array_like shape (..., N), optional
1058
+ Weights describing the relative importance of the transforms. If
1059
+ None (default), then all values in `weights` are assumed to be
1060
+ equal. If given, the shape of `weights` must be broadcastable to
1061
+ the transform shape. Weights must be non-negative.
1062
+ axis : None, int, or tuple of ints, optional
1063
+ Axis or axes along which the means are computed. The default is to
1064
+ compute the mean of all transforms.
1065
+
1066
+ Returns
1067
+ -------
1068
+ mean : `RigidTransform` instance
1069
+ Single transform containing the mean of the transforms in the
1070
+ current instance.
1071
+
1072
+ References
1073
+ ----------
1074
+ .. [1] Hartley, Richard, et al.,
1075
+ "Rotation Averaging", International Journal of Computer Vision
1076
+ 103, 2013, pp. 267-305.
1077
+
1078
+ Examples
1079
+ --------
1080
+ >>> import numpy as np
1081
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1082
+ >>> from scipy.spatial.transform import Rotation as R
1083
+ >>> rng = np.random.default_rng(seed=123)
1084
+
1085
+ The mean of a set of transforms is the same as the mean of the
1086
+ translation and rotation components:
1087
+
1088
+ >>> t = rng.random((4, 3))
1089
+ >>> r = R.random(4, rng=rng)
1090
+ >>> tf = Tf.from_components(t, r)
1091
+ >>> tf.mean().as_matrix()
1092
+ array([[ 0.61593485, -0.74508342, 0.25588075, 0.66999034],
1093
+ [-0.59353615, -0.65246765, -0.47116962, 0.25481794],
1094
+ [ 0.51801458, 0.13833531, -0.84411151, 0.52429339],
1095
+ [0., 0., 0., 1.]])
1096
+ >>> Tf.from_components(t.mean(axis=0), r.mean()).as_matrix()
1097
+ array([[ 0.61593485, -0.74508342, 0.25588075, 0.66999034],
1098
+ [-0.59353615, -0.65246765, -0.47116962, 0.25481794],
1099
+ [ 0.51801458, 0.13833531, -0.84411151, 0.52429339],
1100
+ [0., 0., 0., 1.]])
1101
+ """
1102
+ mean = self._backend.mean(self._matrix, weights=weights, axis=axis)
1103
+ return RigidTransform._from_raw_matrix(mean, xp=self._xp,
1104
+ backend=self._backend)
1105
+
1106
+ def as_matrix(self) -> Array:
1107
+ """Return a copy of the matrix representation of the transform.
1108
+
1109
+ 4x4 rigid transformation matrices are of the form::
1110
+
1111
+ [ tx]
1112
+ [ R ty]
1113
+ [ tz]
1114
+ [ 0 0 0 1]
1115
+
1116
+ where ``R`` is a 3x3 orthonormal rotation matrix and
1117
+ ``t = [tx, ty, tz]`` is a 3x1 translation vector.
1118
+
1119
+ Returns
1120
+ -------
1121
+ matrix : numpy.ndarray, shape (..., 4, 4)
1122
+ Transformation matrices with the same leading dimensions as the transform.
1123
+
1124
+ Examples
1125
+ --------
1126
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1127
+ >>> from scipy.spatial.transform import Rotation as R
1128
+ >>> import numpy as np
1129
+
1130
+ A transformation matrix is a 4x4 matrix formed from a 3x3 rotation
1131
+ matrix and a 3x1 translation vector:
1132
+
1133
+ >>> t = np.array([2, 3, 4])
1134
+ >>> r = R.from_matrix([[0, 0, 1],
1135
+ ... [1, 0, 0],
1136
+ ... [0, 1, 0]])
1137
+ >>> tf = Tf.from_components(t, r)
1138
+ >>> tf.as_matrix()
1139
+ array([[ 0., 0., 1., 2.],
1140
+ [ 1., 0., 0., 3.],
1141
+ [ 0., 1., 0., 4.],
1142
+ [ 0., 0., 0., 1.]])
1143
+
1144
+ >>> Tf.identity(2).as_matrix()
1145
+ array([[[1., 0., 0., 0.],
1146
+ [0., 1., 0., 0.],
1147
+ [0., 0., 1., 0.],
1148
+ [0., 0., 0., 1.]],
1149
+ [[1., 0., 0., 0.],
1150
+ [0., 1., 0., 0.],
1151
+ [0., 0., 1., 0.],
1152
+ [0., 0., 0., 1.]]])
1153
+ """
1154
+ matrix = self._xp.asarray(self._matrix, copy=True)
1155
+ if self._single:
1156
+ return matrix[0, ...]
1157
+ return matrix
1158
+
1159
+ def as_components(self) -> tuple[Array, Rotation]:
1160
+ """Return the translation and rotation components of the transform,
1161
+ where the rotation is applied first, followed by the translation.
1162
+
1163
+ 4x4 rigid transformation matrices are of the form::
1164
+
1165
+ [ tx]
1166
+ [ R ty]
1167
+ [ tz]
1168
+ [ 0 0 0 1]
1169
+
1170
+ Where ``R`` is a 3x3 orthonormal rotation matrix and
1171
+ ``t = [tx, ty, tz]`` is a 3x1 translation vector. This function
1172
+ returns the rotation corresponding to this rotation matrix
1173
+ ``r = Rotation.from_matrix(R)`` and the translation vector ``t``.
1174
+
1175
+ When applying a transform ``tf`` to a vector ``v``, the result is the same
1176
+ as if the rotation and translation components were applied to the vector
1177
+ with the following operation:
1178
+ ``tf.apply(v) == translation + rotation.apply(v)``.
1179
+
1180
+ Returns
1181
+ -------
1182
+ translation : numpy.ndarray, shape (..., 3)
1183
+ The translation of the transform.
1184
+ rotation : `Rotation` instance
1185
+ The rotation of the transform.
1186
+
1187
+ Examples
1188
+ --------
1189
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1190
+ >>> from scipy.spatial.transform import Rotation as R
1191
+ >>> import numpy as np
1192
+
1193
+ Recover the rotation and translation from a transform:
1194
+
1195
+ >>> t = np.array([2, 3, 4])
1196
+ >>> r = R.from_matrix([[0, 0, 1],
1197
+ ... [1, 0, 0],
1198
+ ... [0, 1, 0]])
1199
+ >>> tf = Tf.from_components(t, r)
1200
+ >>> t, r = tf.as_components()
1201
+ >>> t
1202
+ array([2., 3., 4.])
1203
+ >>> r.as_matrix()
1204
+ array([[0., 0., 1.],
1205
+ [1., 0., 0.],
1206
+ [0., 1., 0.]])
1207
+
1208
+ The transform applied to a vector is equivalent to the rotation applied
1209
+ to the vector, followed by the translation:
1210
+
1211
+ >>> t + r.apply([1, 0, 0])
1212
+ array([2., 4., 4.])
1213
+ >>> tf.apply([1, 0, 0])
1214
+ array([2., 4., 4.])
1215
+ """
1216
+ return self.translation, self.rotation
1217
+
1218
+ def as_exp_coords(self) -> Array:
1219
+ """Return the exponential coordinates of the transform.
1220
+
1221
+ This implements the logarithmic map that converts SE(3) to 6-dimensional
1222
+ real vectors.
1223
+
1224
+ This is an inverse of `from_exp_coords` where details on the mapping can
1225
+ be found.
1226
+
1227
+ Returns
1228
+ -------
1229
+ exp_coords : numpy.ndarray, shape (..., 6)
1230
+ Exponential coordinate vectors with the same leading dimensions as the
1231
+ transform. The first three components define the rotation and the last
1232
+ three components define the translation.
1233
+
1234
+ Examples
1235
+ --------
1236
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1237
+ >>> import numpy as np
1238
+
1239
+ Get exponential coordinates of the identity matrix:
1240
+
1241
+ >>> Tf.identity().as_exp_coords()
1242
+ array([0., 0., 0., 0., 0., 0.])
1243
+ """
1244
+ exp_coords = self._backend.as_exp_coords(self._matrix)
1245
+ if self._single:
1246
+ exp_coords = exp_coords[0, ...]
1247
+ return exp_coords
1248
+
1249
+ def as_dual_quat(self, *, scalar_first: bool = False) -> Array:
1250
+ """Return the dual quaternion representation of the transform.
1251
+
1252
+ Unit dual quaternions encode orientation in a real unit quaternion
1253
+ and translation in a dual quaternion. There is a double cover, i.e.,
1254
+ the unit dual quaternions q and -q represent the same transform.
1255
+
1256
+ Parameters
1257
+ ----------
1258
+ scalar_first : bool, optional
1259
+ Whether the scalar component goes first or last in the two
1260
+ individual quaternions that represent the real and the dual part.
1261
+ Default is False, i.e. the scalar-last order is used.
1262
+
1263
+ Returns
1264
+ -------
1265
+ dual_quat : numpy.ndarray, shape (..., 8)
1266
+ Unit dual quaternion vectors with the same leading dimensions as the
1267
+ transform. The real part is stored in the first four components and the
1268
+ dual part in the last four components.
1269
+
1270
+ Examples
1271
+ --------
1272
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1273
+ >>> import numpy as np
1274
+
1275
+ Get identity dual quaternion (we use scalar-last by default):
1276
+
1277
+ >>> Tf.identity().as_dual_quat()
1278
+ array([0., 0., 0., 1., 0., 0., 0., 0.])
1279
+
1280
+ When we want to use the scalar-first convention, we use the argument:
1281
+
1282
+ >>> Tf.identity().as_dual_quat(scalar_first=True)
1283
+ array([1., 0., 0., 0., 0., 0., 0., 0.])
1284
+ """
1285
+ dual_quat = self._backend.as_dual_quat(self._matrix, scalar_first=scalar_first)
1286
+ if self._single:
1287
+ dual_quat = dual_quat[0]
1288
+ return dual_quat
1289
+
1290
+ def __len__(self) -> int:
1291
+ """Return the length of the leading transform dimension.
1292
+
1293
+ A transform can store an N-dimensional array of transforms. The length is the
1294
+ size of the first dimension of this array. If the transform is a single
1295
+ transform, the length is not defined and an error is raised.
1296
+
1297
+ Returns
1298
+ -------
1299
+ length : int
1300
+ The number of transforms in this object.
1301
+
1302
+ Raises
1303
+ ------
1304
+ TypeError
1305
+ If the transform is a single transform.
1306
+
1307
+ Examples
1308
+ --------
1309
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1310
+ >>> import numpy as np
1311
+ >>> tf = Tf.identity(3)
1312
+ >>> len(tf)
1313
+ 3
1314
+
1315
+ An N-dimensional array of transforms returns its first dimension size:
1316
+
1317
+ >>> t = np.ones((5, 2, 3, 1, 3))
1318
+ >>> tf = Tf.from_translation(t)
1319
+ >>> len(tf)
1320
+ 5
1321
+
1322
+ A single transform has no length:
1323
+
1324
+ >>> tf = Tf.from_translation([1, 0, 0])
1325
+ >>> len(tf) # doctest: +IGNORE_EXCEPTION_DETAIL
1326
+ Traceback (most recent call last):
1327
+ ...
1328
+ TypeError: Single transform has no len().
1329
+ """
1330
+ # We don't use self._single here because we also want to raise an error for
1331
+ # Array API backends that call len() on single RigidTransform objects.
1332
+ if self.single:
1333
+ raise TypeError("Single transform has no len")
1334
+ return self._matrix.shape[0]
1335
+
1336
+ def __getitem__(
1337
+ self, indexer: int | slice | EllipsisType | None | ArrayLike
1338
+ ) -> RigidTransform:
1339
+ """Extract transform(s) at given index(es) from this object.
1340
+
1341
+ Creates a new `RigidTransform` instance containing a subset of
1342
+ transforms stored in this object.
1343
+
1344
+ Parameters
1345
+ ----------
1346
+ indexer : int or slice or array_like
1347
+ Specifies which transform(s) to extract. A single indexer must be
1348
+ specified, i.e. as if indexing a 1 dimensional array or list.
1349
+
1350
+ Returns
1351
+ -------
1352
+ transform : `RigidTransform` instance
1353
+ Contains
1354
+ - a single transform, if `indexer` is a single index
1355
+ - a stack of transform(s), if `indexer` is a slice, or an index
1356
+ array.
1357
+
1358
+ Raises
1359
+ ------
1360
+ TypeError
1361
+ If the transform is a single transform.
1362
+
1363
+ Examples
1364
+ --------
1365
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1366
+ >>> t = [[0, 0, 0], [1, 0, 0], [2, 0, 0]] # 3 translations
1367
+ >>> tf = Tf.from_translation(t)
1368
+
1369
+ A single index returns a single transform:
1370
+
1371
+ >>> tf[0].as_matrix()
1372
+ array([[1., 0., 0., 0.],
1373
+ [0., 1., 0., 0.],
1374
+ [0., 0., 1., 0.],
1375
+ [0., 0., 0., 1.]])
1376
+
1377
+ A slice returns a stack of transforms:
1378
+
1379
+ >>> tf[1:3].translation
1380
+ array([[1., 0., 0.],
1381
+ [2., 0., 0.]])
1382
+
1383
+ An index array returns a stack of transforms:
1384
+
1385
+ >>> tf[[0, 2]].translation
1386
+ array([[0., 0., 0.],
1387
+ [2., 0., 0.]])
1388
+ """
1389
+ if self.single:
1390
+ raise TypeError("Single transform is not subscriptable.")
1391
+
1392
+ is_array = isinstance(indexer, type(self._matrix))
1393
+ xp = self._xp
1394
+ # Masking is only specified in the Array API when the array is the sole index
1395
+ # This special case handling is necessary to support boolean indexing and
1396
+ # integer array indexing with take (see
1397
+ # https://github.com/data-apis/array-api/pull/900#issuecomment-2674432480)
1398
+ # Ideally we would converge to [indexer, ...] indexing, but this is not
1399
+ # supported for now.
1400
+ if is_array and indexer.dtype == xp.bool: # type:ignore[union-attr]
1401
+ return RigidTransform(self._matrix[indexer], normalize=False)
1402
+ if is_array and xp.isdtype(indexer.dtype, "integral"): # type:ignore[union-attr]
1403
+ if self._matrix.shape[0] == 0:
1404
+ raise IndexError("cannot take from an empty array")
1405
+ return RigidTransform(
1406
+ xp.take(self._matrix, indexer, axis=0), normalize=False
1407
+ )
1408
+ return RigidTransform(self._matrix[indexer, ...], normalize=False)
1409
+
1410
+ def __setitem__(
1411
+ self,
1412
+ indexer: int | slice | EllipsisType | None | ArrayLike,
1413
+ value: RigidTransform,
1414
+ ):
1415
+ """Set transform(s) at given index(es) in this object.
1416
+
1417
+ Parameters
1418
+ ----------
1419
+ indexer : int or slice or array_like
1420
+ Specifies which transform(s) to replace. A single indexer must be
1421
+ specified, i.e. as if indexing a 1 dimensional array or list.
1422
+
1423
+ value : `RigidTransform` instance
1424
+ The transform(s) to set.
1425
+
1426
+ Raises
1427
+ ------
1428
+ TypeError
1429
+ If the transform is a single transform.
1430
+
1431
+ Examples
1432
+ --------
1433
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1434
+ >>> t = [[0, 0, 0], [1, 0, 0], [2, 0, 0]] # 3 translations
1435
+ >>> tf = Tf.from_translation(t)
1436
+
1437
+ Set a single transform:
1438
+
1439
+ >>> tf[0] = Tf.from_translation([9, 9, 9])
1440
+ >>> tf.translation
1441
+ array([[9., 9., 9.],
1442
+ [1., 0., 0.],
1443
+ [2., 0., 0.]])
1444
+ """
1445
+ if self.single:
1446
+ raise TypeError("Single transform is not subscriptable.")
1447
+
1448
+ if not isinstance(value, RigidTransform):
1449
+ raise TypeError("value must be a RigidTransform object")
1450
+
1451
+ self._matrix = self._backend.setitem(self._matrix, indexer, value.as_matrix())
1452
+
1453
+ def __mul__(
1454
+ self, other: RigidTransform | Rotation
1455
+ ) -> RigidTransform | NotImplementedType:
1456
+ """Compose this transform with the other.
1457
+
1458
+ If ``p`` and ``q`` are two transforms, then the composition of '``q``
1459
+ followed by ``p``' is equivalent to ``p * q``. In terms of
1460
+ transformation matrices, the composition can be expressed as
1461
+ ``p.as_matrix() @ q.as_matrix()``.
1462
+
1463
+ In terms of translations and rotations, the composition when applied to
1464
+ a vector ``v`` is equivalent to
1465
+ ``p.translation + p.rotation.apply(q.translation)
1466
+ + (p.rotation * q.rotation).apply(v)``.
1467
+
1468
+ This function supports composition of multiple transforms at a time using
1469
+ broadcasting rules. The resulting shape for two `RigidTransform` instances
1470
+ ``p`` and ``q`` is `np.broadcast_shapes(p.shape, q.shape)`.
1471
+
1472
+ If ``other`` is a `Rotation`, it is automatically promoted to a
1473
+ `RigidTransform` with zero translation.
1474
+
1475
+ Parameters
1476
+ ----------
1477
+ other : `RigidTransform` or `Rotation` instance
1478
+ Transform(s) or rotation(s) to be composed with this one. The shapes
1479
+ must be broadcastable.
1480
+
1481
+ Returns
1482
+ -------
1483
+ `RigidTransform` instance
1484
+ The composed transform.
1485
+
1486
+ Examples
1487
+ --------
1488
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1489
+ >>> from scipy.spatial.transform import Rotation as R
1490
+ >>> import numpy as np
1491
+
1492
+ Compose two transforms:
1493
+
1494
+ >>> tf1 = Tf.from_translation([1, 0, 0])
1495
+ >>> tf2 = Tf.from_translation([0, 1, 0])
1496
+ >>> tf = tf1 * tf2
1497
+ >>> tf.translation
1498
+ array([1., 1., 0.])
1499
+ >>> tf.single
1500
+ True
1501
+
1502
+ When applied to a vector, the composition of two transforms is applied
1503
+ in right-to-left order.
1504
+
1505
+ >>> t1, r1 = [1, 2, 3], R.from_euler('z', 60, degrees=True)
1506
+ >>> t2, r2 = [0, 1, 0], R.from_euler('x', 30, degrees=True)
1507
+ >>> tf1 = Tf.from_components(t1, r1)
1508
+ >>> tf2 = Tf.from_components(t2, r2)
1509
+ >>> tf = tf1 * tf2
1510
+ >>> tf.apply([1, 0, 0])
1511
+ array([0.6339746, 3.3660254, 3. ])
1512
+ >>> tf1.apply(tf2.apply([1, 0, 0]))
1513
+ array([0.6339746, 3.3660254, 3. ])
1514
+
1515
+ When at least one of the transforms is not single, the result is a stack
1516
+ of transforms.
1517
+
1518
+ >>> tf1 = Tf.from_translation([1, 0, 0])
1519
+ >>> tf2 = Tf.from_translation([[0, 2, 0], [0, 0, 3]])
1520
+ >>> tf = tf1 * tf2
1521
+ >>> tf.translation
1522
+ array([[1., 2., 0.],
1523
+ [1., 0., 3.]])
1524
+ >>> tf.single
1525
+ False
1526
+ >>> len(tf)
1527
+ 2
1528
+
1529
+ Broadcasting rules apply when composing multiple transforms at a time.
1530
+
1531
+ >>> tf1 = Tf.from_translation(np.ones((5, 1, 3))) # Shape (5, 1, 3)
1532
+ >>> tf2 = Tf.from_translation(np.ones((1, 4, 3))) # Shape (1, 4, 3)
1533
+ >>> tf = tf1 * tf2 # Shape (5, 4, 3)
1534
+ >>> tf.translation.shape
1535
+ (5, 4, 3)
1536
+ """
1537
+ if isinstance(other, Rotation):
1538
+ other = RigidTransform.from_rotation(other)
1539
+ elif not isinstance(other, RigidTransform):
1540
+ # If other is not a RigidTransform, we return NotImplemented to allow other
1541
+ # types to implement __rmul__
1542
+ return NotImplemented
1543
+ if not broadcastable(self._matrix.shape, other._matrix.shape):
1544
+ raise ValueError(
1545
+ f"Cannot broadcast {self._matrix.shape[:-2]} transforms in "
1546
+ f"first to {other._matrix.shape[:-2]} transforms in second object."
1547
+ )
1548
+ cython_compatible = self._matrix.ndim < 4 and other._matrix.ndim < 4
1549
+ backend = select_backend(self._xp, cython_compatible=cython_compatible)
1550
+ matrix = backend.compose_transforms(self._matrix, other._matrix)
1551
+ # Only necessary for cython. Array API broadcasting handles this by default
1552
+ if self._single and other._single:
1553
+ matrix = matrix[0, ...]
1554
+ return RigidTransform(matrix, normalize=True, copy=False)
1555
+
1556
+ def __rmul__(self, other: Rotation) -> RigidTransform | NotImplementedType:
1557
+ """Compose a rotation with this transform (rotation applied second).
1558
+
1559
+ See `__mul__` for more details.
1560
+
1561
+ Parameters
1562
+ ----------
1563
+ other : `Rotation` instance
1564
+ The rotation to compose with this transform. The shapes must be
1565
+ broadcastable.
1566
+
1567
+ Returns
1568
+ -------
1569
+ `RigidTransform` instance
1570
+ The composed transform.
1571
+ """
1572
+ if isinstance(other, Rotation):
1573
+ return RigidTransform.from_rotation(other) * self
1574
+ # When other is a RigidTransform __mul__ is called, so we don't handle it here
1575
+ return NotImplemented
1576
+
1577
+ def __pow__(self, n: float) -> RigidTransform:
1578
+ """Compose this transform with itself `n` times.
1579
+
1580
+ A rigid transform `p` when raised to non-integer powers can be thought
1581
+ of as finding a fraction of the transformation. For example, a power of
1582
+ 0.5 finds a "halfway" transform from the identity to `p`.
1583
+
1584
+ This is implemented by applying screw linear interpolation (ScLERP)
1585
+ between `p` and the identity transform, where the angle of the rotation
1586
+ component is scaled by `n`, and the translation is proportionally
1587
+ adjusted along the screw axis.
1588
+
1589
+ ``q = p ** n`` can also be expressed as
1590
+ ``q = RigidTransform.from_exp_coords(p.as_exp_coords() * n)``.
1591
+
1592
+ If `n` is negative, then the transform is inverted before the power
1593
+ is applied. In other words, ``p ** -abs(n) == p.inv() ** abs(n)``.
1594
+
1595
+ Parameters
1596
+ ----------
1597
+ n : float
1598
+ The number of times to compose the transform with itself.
1599
+
1600
+ Returns
1601
+ -------
1602
+ `RigidTransform` instance
1603
+ If the input Rotation `p` contains `N` multiple rotations, then
1604
+ the output will contain `N` rotations where the `i` th rotation
1605
+ is equal to ``p[i] ** n``.
1606
+
1607
+ Notes
1608
+ -----
1609
+ There are three notable cases: if ``n == 1`` then a copy of the original
1610
+ transform is returned, if ``n == 0`` then the identity transform is
1611
+ returned, and if ``n == -1`` then the inverse transform is returned.
1612
+
1613
+ Note that fractional powers ``n`` which effectively take a root of
1614
+ rotation, do so using the shortest path smallest representation of that
1615
+ angle (the principal root). This means that powers of ``n`` and ``1/n``
1616
+ are not necessarily inverses of each other. For example, a 0.5 power of
1617
+ a +240 degree rotation will be calculated as the 0.5 power of a -120
1618
+ degree rotation, with the result being a rotation of -60 rather than
1619
+ +120 degrees.
1620
+
1621
+ Examples
1622
+ --------
1623
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1624
+ >>> import numpy as np
1625
+
1626
+ A power of 2 returns the transform composed with itself:
1627
+
1628
+ >>> tf = Tf.from_translation([1, 2, 3])
1629
+ >>> (tf ** 2).translation
1630
+ array([2., 4., 6.])
1631
+ >>> (tf ** 2).as_matrix()
1632
+ array([[1., 0., 0., 2.],
1633
+ [0., 1., 0., 4.],
1634
+ [0., 0., 1., 6.],
1635
+ [0., 0., 0., 1.]])
1636
+
1637
+ A negative power returns the inverse of the transform raised to the
1638
+ absolute value of `n`:
1639
+
1640
+ >>> (tf ** -2).translation
1641
+ array([-2., -4., -6.])
1642
+ >>> np.allclose((tf ** -2).as_matrix(), (tf.inv() ** 2).as_matrix(),
1643
+ ... atol=1e-12)
1644
+ True
1645
+
1646
+ A power of 0 returns the identity transform:
1647
+
1648
+ >>> (tf ** 0).as_matrix()
1649
+ array([[1., 0., 0., 0.],
1650
+ [0., 1., 0., 0.],
1651
+ [0., 0., 1., 0.],
1652
+ [0., 0., 0., 1.]])
1653
+
1654
+ A power of 1 returns a copy of the original transform:
1655
+
1656
+ >>> (tf ** 1).as_matrix()
1657
+ array([[1., 0., 0., 1.],
1658
+ [0., 1., 0., 2.],
1659
+ [0., 0., 1., 3.],
1660
+ [0., 0., 0., 1.]])
1661
+
1662
+ A fractional power returns a transform with a scaled rotation and
1663
+ translated along the screw axis. Here we take the square root of the
1664
+ transform, which when squared recovers the original transform:
1665
+
1666
+ >>> tf_half = (tf ** 0.5)
1667
+ >>> tf_half.translation
1668
+ array([0.5, 1., 1.5])
1669
+ >>> (tf_half ** 2).as_matrix()
1670
+ array([[1., 0., 0., 1.],
1671
+ [0., 1., 0., 2.],
1672
+ [0., 0., 1., 3.],
1673
+ [0., 0., 0., 1.]])
1674
+ """
1675
+ matrix = self._backend.pow(self._matrix, n)
1676
+ if self._single:
1677
+ matrix = matrix[0, ...]
1678
+ return RigidTransform._from_raw_matrix(matrix, self._xp, self._backend)
1679
+
1680
+ def inv(self) -> RigidTransform:
1681
+ """Invert this transform.
1682
+
1683
+ Composition of a transform with its inverse results in an identity
1684
+ transform.
1685
+
1686
+ A rigid transform is a composition of a rotation and a translation,
1687
+ where the rotation is applied first, followed by the translation. So the
1688
+ inverse transform is equivalent to the inverse translation followed by
1689
+ the inverse rotation.
1690
+
1691
+ Returns
1692
+ -------
1693
+ `RigidTransform` instance
1694
+ The inverse of this transform.
1695
+
1696
+ Examples
1697
+ --------
1698
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1699
+ >>> from scipy.spatial.transform import Rotation as R
1700
+ >>> import numpy as np
1701
+
1702
+ A transform composed with its inverse results in an identity transform:
1703
+
1704
+ >>> rng = np.random.default_rng(seed=123)
1705
+ >>> t = rng.random(3)
1706
+ >>> r = R.random(rng=rng)
1707
+ >>> tf = Tf.from_components(t, r)
1708
+ >>> tf.as_matrix()
1709
+ array([[-0.45431291, 0.67276178, -0.58394466, 0.68235186],
1710
+ [-0.23272031, 0.54310598, 0.80676958, 0.05382102],
1711
+ [ 0.85990758, 0.50242162, -0.09017473, 0.22035987],
1712
+ [ 0. , 0. , 0. , 1. ]])
1713
+
1714
+ >>> (tf.inv() * tf).as_matrix()
1715
+ array([[[1., 0., 0., 0.],
1716
+ [0., 1., 0., 0.],
1717
+ [0., 0., 1., 0.],
1718
+ [0., 0., 0., 1.]]])
1719
+
1720
+ The inverse rigid transform is the same as the inverse translation
1721
+ followed by the inverse rotation:
1722
+
1723
+ >>> t, r = tf.as_components()
1724
+ >>> r_inv = r.inv() # inverse rotation
1725
+ >>> t_inv = -t # inverse translation
1726
+ >>> tf_r_inv = Tf.from_rotation(r_inv)
1727
+ >>> tf_t_inv = Tf.from_translation(t_inv)
1728
+ >>> np.allclose((tf_r_inv * tf_t_inv).as_matrix(),
1729
+ ... tf.inv().as_matrix(),
1730
+ ... atol=1e-12)
1731
+ True
1732
+ >>> (tf_r_inv * tf_t_inv * tf).as_matrix()
1733
+ array([[[1., 0., 0., 0.],
1734
+ [0., 1., 0., 0.],
1735
+ [0., 0., 1., 0.],
1736
+ [0., 0., 0., 1.]]])
1737
+ """
1738
+ matrix = self._backend.inv(self._matrix)
1739
+ if self._single:
1740
+ matrix = matrix[0, ...]
1741
+ return RigidTransform._from_raw_matrix(matrix, self._xp, self._backend)
1742
+
1743
+ def apply(self, vector: ArrayLike, inverse: bool = False) -> Array:
1744
+ """Apply the transform to a vector.
1745
+
1746
+ If the original frame transforms to the final frame by this transform,
1747
+ then its application to a vector can be seen in two ways:
1748
+
1749
+ - As a projection of vector components expressed in the final frame
1750
+ to the original frame.
1751
+ - As the physical transformation of a vector being glued to the
1752
+ original frame as it transforms. In this case the vector
1753
+ components are expressed in the original frame before and after
1754
+ the transformation.
1755
+
1756
+ In terms of the rotation matrix and translation,
1757
+ this application is the same as
1758
+ ``self.translation + (mat @ vector[..., np.newaxis])[..., 0]``,
1759
+ where ``mat = self.rotation.as_matrix()``.
1760
+
1761
+ For a single transform, this is the same as
1762
+ ``self.translation + vector @ self.rotation.as_matrix().T``.
1763
+
1764
+ Parameters
1765
+ ----------
1766
+ vector : array_like, shape (..., 3)
1767
+ Vector(s) to be transformed. Each vector[..., :] represents a 3D
1768
+ vector.
1769
+ inverse : bool, optional
1770
+ If True, the inverse of the transform is applied to the vector.
1771
+
1772
+ Returns
1773
+ -------
1774
+ transformed_vector : numpy.ndarray, shape (..., 3)
1775
+ The transformed vector(s) with shape determined by broadcasting
1776
+ the transform and vector shapes together.
1777
+
1778
+ Examples
1779
+ --------
1780
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1781
+ >>> from scipy.spatial.transform import Rotation as R
1782
+ >>> import numpy as np
1783
+
1784
+ Apply a single transform to a vector. Here the transform is just a
1785
+ translation, so the result is the vector added to the translation
1786
+ vector.
1787
+
1788
+ >>> t = np.array([1, 2, 3])
1789
+ >>> tf = Tf.from_translation(t)
1790
+ >>> t + np.array([1, 0, 0])
1791
+ array([2, 2, 3])
1792
+ >>> tf.apply([1, 0, 0])
1793
+ array([2., 2., 3.])
1794
+
1795
+ Apply a single transform to a stack of vectors:
1796
+
1797
+ >>> tf.apply([[1, 0, 0], [0, 1, 0]])
1798
+ array([[2., 2., 3.],
1799
+ [1., 3., 3.]])
1800
+
1801
+ Apply the inverse of a transform to a vector, so the result is the
1802
+ negative of the translation vector added to the vector.
1803
+
1804
+ >>> -t + np.array([1, 0, 0])
1805
+ array([0, -2, -3])
1806
+ >>> tf.apply([1, 0, 0], inverse=True)
1807
+ array([0., -2., -3.])
1808
+
1809
+ Broadcasting is supported when applying multiple transforms to an N-dimensional
1810
+ array of vectors.
1811
+
1812
+ >>> tf = Tf.from_translation(np.ones((4, 5, 1, 3)))
1813
+ >>> vectors = np.zeros((2, 3))
1814
+ >>> tf.apply(vectors).shape
1815
+ (4, 5, 2, 3)
1816
+
1817
+ For transforms which are not just pure translations, applying it to a
1818
+ vector is the same as applying the rotation component to the vector and
1819
+ then adding the translation component.
1820
+
1821
+ >>> r = R.from_euler('z', 60, degrees=True)
1822
+ >>> tf = Tf.from_components(t, r)
1823
+ >>> t + r.apply([1, 0, 0])
1824
+ array([1.5, 2.8660254, 3. ])
1825
+ >>> tf.apply([1, 0, 0])
1826
+ array([1.5, 2.8660254, 3. ])
1827
+
1828
+ When applying the inverse of a transform, the result is the negative of
1829
+ the translation vector added to the vector, and then rotated by the
1830
+ inverse rotation.
1831
+
1832
+ >>> r.inv().apply(-t + np.array([1, 0, 0]))
1833
+ array([-1.73205081, -1. , -3. ])
1834
+ >>> tf.apply([1, 0, 0], inverse=True)
1835
+ array([-1.73205081, -1. , -3. ])
1836
+ """
1837
+ # We do not use the cached xp here to catch cases where matrix and vector have
1838
+ # different xp instances.
1839
+ xp = array_namespace(self._matrix, vector)
1840
+ vector = xp.asarray(
1841
+ vector, dtype=self._matrix.dtype, device=device(self._matrix)
1842
+ )
1843
+ cython_compatible = self._matrix.ndim < 4 and vector.ndim < 3
1844
+ backend = select_backend(xp, cython_compatible=cython_compatible)
1845
+ result = backend.apply(self._matrix, vector, inverse)
1846
+ if self._single and vector.ndim == 1:
1847
+ result = result[0, ...]
1848
+ return result
1849
+
1850
+ @property
1851
+ def rotation(self) -> Rotation:
1852
+ """Return the rotation component of the transform.
1853
+
1854
+ A transform is a composition of a rotation and a translation, such that
1855
+ when applied to a vector, the vector is first rotated and then
1856
+ translated. This property returns the rotation part of the transform.
1857
+
1858
+ Returns
1859
+ -------
1860
+ rotation : `Rotation` instance
1861
+ A single rotation or a stack of rotations.
1862
+
1863
+ Examples
1864
+ --------
1865
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1866
+ >>> from scipy.spatial.transform import Rotation as R
1867
+ >>> import numpy as np
1868
+
1869
+ The rotation component is extracted from the transform:
1870
+
1871
+ >>> t = np.array([1, 0, 0])
1872
+ >>> r = R.random(3)
1873
+ >>> tf = Tf.from_components(t, r)
1874
+ >>> np.allclose(tf.rotation.as_matrix(), r.as_matrix())
1875
+ True
1876
+ """
1877
+ if self._single:
1878
+ return Rotation.from_matrix(self._matrix[0, :3, :3])
1879
+ return Rotation.from_matrix(self._matrix[..., :3, :3])
1880
+
1881
+ @property
1882
+ def translation(self) -> Array:
1883
+ """Return the translation component of the transform.
1884
+
1885
+ A transform is a composition of a rotation and a translation, such that
1886
+ when applied to a vector, the vector is first rotated and then
1887
+ translated. This property returns the translation part of the transform.
1888
+
1889
+ Returns
1890
+ -------
1891
+ translation : numpy.ndarray, shape (..., 3)
1892
+ Translation vectors with the same leading dimensions as the transform.
1893
+
1894
+ Examples
1895
+ --------
1896
+ >>> from scipy.spatial.transform import RigidTransform as Tf
1897
+ >>> from scipy.spatial.transform import Rotation as R
1898
+ >>> import numpy as np
1899
+
1900
+ The translation component is extracted from the transform:
1901
+
1902
+ >>> t = np.array([[1, 0, 0], [2, 0, 0], [3, 0, 0]])
1903
+ >>> r = R.random()
1904
+ >>> tf = Tf.from_components(t, r)
1905
+ >>> np.allclose(tf.translation, t)
1906
+ True
1907
+ """
1908
+ if self._single:
1909
+ return self._xp.asarray(self._matrix[0, :3, 3], copy=True)
1910
+ return self._xp.asarray(self._matrix[..., :3, 3], copy=True)
1911
+
1912
+ @property
1913
+ def single(self) -> bool:
1914
+ """Whether this instance represents a single transform.
1915
+
1916
+ Single transforms are not subscriptable, and do not have a length.
1917
+
1918
+ Returns
1919
+ -------
1920
+ single : bool
1921
+ True if this instance represents a single transform, False
1922
+ otherwise.
1923
+ """
1924
+ return self._single or self._matrix.ndim == 2
1925
+
1926
+ @property
1927
+ def shape(self) -> tuple[int, ...]:
1928
+ """The shape of the transform's leading dimensions."""
1929
+ if self._single or self._matrix.ndim == 2:
1930
+ return ()
1931
+ return self._matrix.shape[:-2]
1932
+
1933
+ def __reduce__(self) -> tuple[Callable, tuple]:
1934
+ """Reduce the RigidTransform for pickling.
1935
+
1936
+ We store modules inside RigidTransforms which cannot be pickled. To circumvent
1937
+ this, we pickle only the matrix and restore the cached modules from the matrix
1938
+ type in `from_matrix`.
1939
+ """
1940
+ matrix = self._matrix
1941
+ if self._single:
1942
+ matrix = matrix[0, ...]
1943
+ return (self.__class__.from_matrix, (matrix,))
1944
+
1945
+ def __iter__(self) -> Iterator[RigidTransform]:
1946
+ """Iterate over transforms."""
1947
+ if self._single or self._matrix.ndim == 2:
1948
+ raise TypeError("Single transform is not iterable.")
1949
+ # We return a generator that yields a new RigidTransform object for each
1950
+ # transform in the current object. We cannot rely on the default implementation
1951
+ # because jax will not raise an IndexError for out-of-bounds indices.
1952
+ for i in range(self._matrix.shape[0]):
1953
+ yield RigidTransform._from_raw_matrix(
1954
+ self._matrix[i, ...], self._xp, self._backend
1955
+ )
1956
+
1957
+ @staticmethod
1958
+ def _from_raw_matrix(
1959
+ matrix: Array, xp: ModuleType, backend: ModuleType | None = None
1960
+ ) -> RigidTransform:
1961
+ """Create a RigidTransform skipping all sanitization steps.
1962
+
1963
+ This method is is intended for internal, performant creation of RigidTransforms
1964
+ with matrices that are guaranteed to be valid.
1965
+ """
1966
+ tf = RigidTransform.__new__(RigidTransform)
1967
+ tf._single = matrix.ndim == 2 and is_numpy(xp)
1968
+ if tf._single:
1969
+ matrix = xpx.atleast_nd(matrix, ndim=3, xp=xp)
1970
+ tf._matrix = matrix
1971
+ tf._xp = xp
1972
+ if backend is None:
1973
+ backend = select_backend(xp, matrix.ndim < 4)
1974
+ tf._backend = backend
1975
+ return tf
v12x/pylib/scipy/spatial/transform/_rigid_transform_xp.py ADDED
@@ -0,0 +1,448 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from types import EllipsisType
2
+
3
+ from scipy._lib._array_api import (
4
+ array_namespace,
5
+ Array,
6
+ ArrayLike,
7
+ is_lazy_array,
8
+ xp_vector_norm,
9
+ xp_result_type,
10
+ xp_promote,
11
+ is_array_api_obj,
12
+ )
13
+ import scipy._external.array_api_extra as xpx
14
+ from scipy.spatial.transform._rotation_xp import (
15
+ as_matrix as quat_as_matrix,
16
+ from_matrix as quat_from_matrix,
17
+ _from_matrix_orthogonal as quat_from_matrix_orthogonal,
18
+ from_rotvec as quat_from_rotvec,
19
+ as_rotvec as quat_as_rotvec,
20
+ compose_quat,
21
+ from_quat,
22
+ inv as quat_inv,
23
+ mean as quat_mean,
24
+ )
25
+ from scipy._external.array_api_compat import device as xp_device
26
+ from scipy._lib._util import broadcastable
27
+
28
+
29
+ def from_matrix(matrix: Array, normalize: bool = True, copy: bool = True) -> Array:
30
+ xp = array_namespace(matrix)
31
+ # Shape check should be done before calling this function
32
+ if normalize or copy:
33
+ matrix = xp.asarray(matrix, copy=True)
34
+
35
+ last_row_ok = xp.all(
36
+ matrix[..., 3, :] == xp.asarray([0, 0, 0, 1.0], device=xp_device(matrix)),
37
+ axis=-1,
38
+ )
39
+ lazy = is_lazy_array(matrix)
40
+ # We delay lazy branch checks until after normalization to avoid overwriting nans
41
+ # with the rotation matrix
42
+ if not lazy and xp.any(~last_row_ok):
43
+ if last_row_ok.shape == ():
44
+ idx = ()
45
+ else:
46
+ idx = tuple(int(i[0]) for i in xp.nonzero(~last_row_ok))
47
+ vals = matrix[idx + (3, ...)]
48
+ raise ValueError(
49
+ f"Expected last row of transformation matrix {idx} to be "
50
+ f"exactly [0, 0, 0, 1], got {vals}"
51
+ )
52
+
53
+ # The quat_from_matrix() method orthogonalizes the rotation
54
+ # component of the transformation matrix. While this does have some
55
+ # overhead in converting a rotation matrix to a quaternion and back, it
56
+ # allows for skipping singular value decomposition for near-orthogonal
57
+ # matrices, which is a computationally expensive operation.
58
+ if normalize:
59
+ rotmat = quat_as_matrix(quat_from_matrix(matrix[..., :3, :3]))
60
+ matrix = xpx.at(matrix)[..., :3, :3].set(rotmat)
61
+ # Lazy branch matrix invalidation
62
+ if lazy:
63
+ matrix = xp.where(last_row_ok[..., None, None], matrix, xp.nan)
64
+ return matrix
65
+
66
+
67
+ def from_rotation(quat: Array) -> Array:
68
+ xp = array_namespace(quat)
69
+ rotmat = quat_as_matrix(quat)
70
+ matrix = xp.zeros(
71
+ (*rotmat.shape[:-2], 4, 4), dtype=quat.dtype, device=xp_device(quat)
72
+ )
73
+ matrix = xpx.at(matrix)[..., :3, :3].set(rotmat)
74
+ matrix = xpx.at(matrix)[..., 3, 3].set(1)
75
+ return matrix
76
+
77
+
78
+ def from_translation(translation: Array) -> Array:
79
+ xp = array_namespace(translation)
80
+
81
+ if translation.shape[-1] != 3:
82
+ raise ValueError(
83
+ f"Expected `translation` to have shape (..., 3), got {translation.shape}."
84
+ )
85
+ device = xp_device(translation)
86
+ dtype = xp_result_type(translation, force_floating=True, xp=xp)
87
+ eye = xp.eye(4, dtype=dtype, device=device)
88
+
89
+ matrix = xpx.atleast_nd(eye, ndim=translation.ndim + 1, xp=xp)
90
+ matrix = xp.zeros(
91
+ (*translation.shape[:-1], 4, 4),
92
+ dtype=dtype,
93
+ device=device,
94
+ )
95
+ matrix = xpx.at(matrix)[...].set(xp.eye(4, dtype=dtype, device=device))
96
+ matrix = xpx.at(matrix)[..., :3, 3].set(
97
+ xp_promote(translation, force_floating=True, xp=xp)
98
+ )
99
+ return matrix
100
+
101
+
102
+ def from_components(translation: Array, quat: Array) -> Array:
103
+ return compose_transforms(from_translation(translation), from_rotation(quat))
104
+
105
+
106
+ def from_exp_coords(exp_coords: Array) -> Array:
107
+ rot_vec = exp_coords[..., :3]
108
+ rot_matrix = quat_as_matrix(quat_from_rotvec(rot_vec))
109
+ translation_transform = _compute_se3_exp_translation_transform(rot_vec)
110
+ translations = (translation_transform @ exp_coords[..., 3:, None])[..., 0]
111
+ return _create_transformation_matrix(translations, rot_matrix)
112
+
113
+
114
+ def from_dual_quat(dual_quat: Array, *, scalar_first: bool = False) -> Array:
115
+ xp = array_namespace(dual_quat)
116
+
117
+ if dual_quat.shape[-1] != 8:
118
+ raise ValueError(
119
+ f"Expected `dual_quat` to have shape (..., 8), got {dual_quat.shape}."
120
+ )
121
+
122
+ real_part = dual_quat[..., :4]
123
+ dual_part = dual_quat[..., 4:]
124
+ if scalar_first:
125
+ real_part = xp.roll(real_part, -1, axis=-1)
126
+ dual_part = xp.roll(dual_part, -1, axis=-1)
127
+
128
+ real_part, dual_part = _normalize_dual_quaternion(real_part, dual_part)
129
+
130
+ rot_quat = from_quat(real_part)
131
+
132
+ translation = 2.0 * compose_quat(dual_part, quat_inv(rot_quat))[..., :3]
133
+ matrix = _create_transformation_matrix(translation, quat_as_matrix(rot_quat))
134
+
135
+ return matrix
136
+
137
+
138
+ def as_exp_coords(matrix: Array) -> Array:
139
+ xp = array_namespace(matrix)
140
+ rot_vec = quat_as_rotvec(quat_from_matrix_orthogonal(matrix[..., :3, :3]))
141
+ translation_transform = _compute_se3_log_translation_transform(rot_vec)
142
+ translations = (translation_transform @ matrix[..., :3, 3][..., None])[..., 0]
143
+ exp_coords = xp.concat([rot_vec, translations], axis=-1)
144
+ return exp_coords
145
+
146
+
147
+ def as_dual_quat(matrix: Array, *, scalar_first: bool = False) -> Array:
148
+ xp = array_namespace(matrix)
149
+ real_parts = quat_from_matrix_orthogonal(matrix[..., :3, :3])
150
+
151
+ pure_translation_quats = xp.empty(
152
+ (*matrix.shape[:-2], 4), dtype=matrix.dtype, device=xp_device(matrix)
153
+ )
154
+ pure_translation_quats = xpx.at(pure_translation_quats)[..., :3].set(
155
+ matrix[..., :3, 3]
156
+ )
157
+ pure_translation_quats = xpx.at(pure_translation_quats)[..., 3].set(0.0)
158
+
159
+ dual_parts = 0.5 * compose_quat(pure_translation_quats, real_parts)
160
+
161
+ if scalar_first:
162
+ real_parts = xp.roll(real_parts, 1, axis=-1)
163
+ dual_parts = xp.roll(dual_parts, 1, axis=-1)
164
+
165
+ dual_quats = xp.concat([real_parts, dual_parts], axis=-1)
166
+ return dual_quats
167
+
168
+
169
+ def compose_transforms(tf_matrix: Array, other_tf_matrix: Array) -> Array:
170
+ if not broadcastable(tf_matrix.shape, other_tf_matrix.shape):
171
+ len_a = tf_matrix.shape[0] if tf_matrix.ndim == 3 else 1
172
+ len_b = other_tf_matrix.shape[0] if other_tf_matrix.ndim == 3 else 1
173
+ raise ValueError(
174
+ "Expected equal number of transforms in both or a "
175
+ "single transform in either object, got "
176
+ f"{len_a} transforms in first and {len_b}"
177
+ "transforms in second object."
178
+ )
179
+ return tf_matrix @ other_tf_matrix
180
+
181
+
182
+ def inv(matrix: Array) -> Array:
183
+ xp = array_namespace(matrix)
184
+ r_inv = xp.matrix_transpose(matrix[..., :3, :3])
185
+ # Matrix multiplication of r_inv and translation vector
186
+ t_inv = -(r_inv @ matrix[..., :3, 3][..., None])[..., 0]
187
+ matrix = xp.zeros(
188
+ (*matrix.shape[:-2], 4, 4), dtype=matrix.dtype, device=xp_device(matrix)
189
+ )
190
+ matrix = xpx.at(matrix)[..., :3, :3].set(r_inv)
191
+ matrix = xpx.at(matrix)[..., :3, 3].set(t_inv)
192
+ matrix = xpx.at(matrix)[..., 3, 3].set(1)
193
+ return matrix
194
+
195
+
196
+ def apply(matrix: Array, vector: Array, inverse: bool = False) -> Array:
197
+ xp = array_namespace(matrix)
198
+ if vector.shape[-1] != 3:
199
+ raise ValueError(f"Expected vector to have shape (..., 3), got {vector.shape}.")
200
+ vec = xp.empty(
201
+ (*vector.shape[:-1], 4), dtype=vector.dtype, device=xp_device(vector)
202
+ )
203
+ vec = xpx.at(vec)[..., :3].set(vector)
204
+ vec = xpx.at(vec)[..., 3].set(1)
205
+ vec = vec[..., None]
206
+
207
+ if inverse:
208
+ matrix = inv(matrix)
209
+
210
+ # We raise a ValueError manually here because letting the function run its course
211
+ # would raise heterogeneous error types and messages for different frameworks.
212
+ # However, the error only mimics numpy's error message and does not provide the
213
+ # same amount of context.
214
+ if not broadcastable(matrix.shape, vec.shape): # type:ignore[arg-type]
215
+ raise ValueError("operands could not be broadcast together")
216
+ return (matrix @ vec)[..., :3, 0]
217
+
218
+
219
+ def pow(matrix: Array, n: float | Array) -> Array:
220
+ xp = array_namespace(matrix)
221
+ device = xp_device(matrix)
222
+ # If n is an array, we sanitize it to a scalar and promote quat and n to
223
+ # the same dtype.
224
+ if is_array_api_obj(n):
225
+ if n.shape == (1,): # type:ignore[union-attr]
226
+ n = n[0] # type:ignore[index]
227
+ elif n.ndim != 0: # type:ignore[union-attr]
228
+ raise ValueError("Array exponent must be a scalar")
229
+ matrix, n = xp_promote(matrix, n, force_floating=True, xp=xp)
230
+
231
+ # If n is a lazy array, we cannot take fast paths for special cases.
232
+ if is_lazy_array(n):
233
+ # Lazy execution. We compute all special cases and the general case
234
+ result = from_exp_coords(as_exp_coords(matrix) * n)
235
+ identity = xp.eye(4, dtype=matrix.dtype, device=device)
236
+ result = xp.where(n == 0, identity, result)
237
+ result = xp.where(n == -1, inv(matrix), result)
238
+ result = xp.where(n == 1, matrix, result)
239
+ return result
240
+ if n == 0:
241
+ identity = xp.eye(4, dtype=matrix.dtype, device=device)
242
+ identity = xpx.atleast_nd(identity, ndim=matrix.ndim, xp=xp)
243
+ return xp.tile(identity, (*matrix.shape[:-2], 1, 1))
244
+ elif n == -1:
245
+ return inv(matrix)
246
+ elif n == 1:
247
+ return matrix
248
+ return from_exp_coords(as_exp_coords(matrix) * n)
249
+
250
+
251
+ def mean(
252
+ matrix: Array,
253
+ weights: ArrayLike | None = None,
254
+ axis: None | int | tuple[int, ...] = None
255
+ ) -> Array:
256
+ xp = array_namespace(matrix)
257
+ if matrix.shape[0] == 0:
258
+ raise ValueError("Mean of an empty rotation set is undefined.")
259
+ # Axis logic: For None, we reduce over all axes. For int, we only reduce over that
260
+ # axis. For tuple, we reduce over all specified axes.
261
+ all_axes = tuple(range(matrix.ndim - 2))
262
+ if axis is None:
263
+ axis = all_axes
264
+ elif isinstance(axis, int):
265
+ axis = (axis,)
266
+ if not isinstance(axis, tuple):
267
+ raise ValueError("`axis` must be None, int, or tuple of ints.")
268
+ # Ensure all axes are within bounds
269
+ if (axis != () and
270
+ (min(axis) < -(matrix.ndim - 2) or max(axis) > (matrix.ndim - 3))
271
+ ):
272
+ raise ValueError(
273
+ f"axis {axis} is out of bounds for transform with shape "
274
+ f"{matrix.shape[:-2]}."
275
+ )
276
+ # Ensure all axes are positive and unique
277
+ axis = tuple(sorted(set(x % (matrix.ndim - 2) for x in axis)))
278
+
279
+ lazy = is_lazy_array(matrix)
280
+ quats = quat_from_matrix_orthogonal(matrix[..., :3, :3])
281
+ if weights is None:
282
+ quats_mean = quat_mean(quats, axis=axis)
283
+ else:
284
+ neg_weights = weights < 0 # type:ignore[operator]
285
+ any_neg_weights = xp.any(neg_weights)
286
+ if not lazy and any_neg_weights:
287
+ raise ValueError("`weights` must be non-negative.")
288
+ if weights.shape != matrix.shape[:-2]: # type:ignore[union-attr]
289
+ raise ValueError(
290
+ f"Expected `weights` to match transform shape, got shape "
291
+ f"{weights.shape} for {matrix.shape[:-2]} transformations." # type:ignore[union-attr]
292
+ )
293
+ quats_mean = quat_mean(quats, weights=weights, axis=axis)
294
+ r_mean = quat_as_matrix(quats_mean)
295
+
296
+ t = matrix[..., :3, 3]
297
+ if weights is None:
298
+ t_mean = xp.mean(t, axis=axis)
299
+ else:
300
+ norm = xp.sum(weights[..., None], axis=axis) # type:ignore[index]
301
+ wsum = xp.sum(t * weights[..., None], axis=axis) # type:ignore[index]
302
+ t_mean = wsum / norm
303
+
304
+ tf = _create_transformation_matrix(t_mean, r_mean)
305
+ if weights is not None and lazy:
306
+ # We cannot raise on negative weights because jit code needs to be
307
+ # non-branching. We return NaN instead
308
+ mask = xp.where(any_neg_weights, xp.nan, 1.0) # pyrefly:ignore[unbound-name]
309
+ tf = mask * tf
310
+ return tf
311
+
312
+
313
+ def setitem(
314
+ matrix: Array,
315
+ indexer: Array | int | tuple | slice | EllipsisType | None,
316
+ value: Array,
317
+ ) -> Array:
318
+ xp = array_namespace(matrix)
319
+ if isinstance(indexer, EllipsisType):
320
+ return xpx.at(matrix)[indexer].set(value)
321
+ if is_array_api_obj(indexer) and indexer.dtype == xp.bool: # type:ignore[union-attr]
322
+ return xpx.at(matrix)[indexer].set(value) # type:ignore[index]
323
+ return xpx.at(matrix)[indexer, ...].set(value) # type:ignore[index]
324
+
325
+
326
+ def normalize_dual_quaternion(dual_quat: Array) -> Array:
327
+ """Normalize dual quaternion."""
328
+ xp = array_namespace(dual_quat)
329
+ real, dual = _normalize_dual_quaternion(dual_quat[..., :4], dual_quat[..., 4:])
330
+ return xp.concat((real, dual), axis=-1)
331
+
332
+
333
+ def _create_transformation_matrix(
334
+ translations: Array, rotation_matrices: Array
335
+ ) -> Array:
336
+ if not translations.shape[:-1] == rotation_matrices.shape[:-2]:
337
+ raise ValueError(
338
+ "The number of rotation matrices and translations must be the same."
339
+ )
340
+ xp = array_namespace(translations)
341
+ matrix = xp.empty(
342
+ (*translations.shape[:-1], 4, 4),
343
+ dtype=translations.dtype,
344
+ device=xp_device(translations),
345
+ )
346
+ matrix = xpx.at(matrix)[..., :3, :3].set(rotation_matrices)
347
+ matrix = xpx.at(matrix)[..., :3, 3].set(translations)
348
+ matrix = xpx.at(matrix)[..., 3, :3].set(0)
349
+ matrix = xpx.at(matrix)[..., 3, 3].set(1)
350
+ return matrix
351
+
352
+
353
+ def _compute_se3_exp_translation_transform(rot_vec: Array) -> Array:
354
+ """Compute the transformation matrix from the se3 translation part to SE3
355
+ translation.
356
+
357
+ The transformation matrix depends on the rotation vector.
358
+ """
359
+ xp = array_namespace(rot_vec)
360
+ device = xp_device(rot_vec)
361
+ dtype = rot_vec.dtype
362
+ angle = xp_vector_norm(rot_vec, axis=-1, keepdims=True, xp=xp)
363
+ small_scale = angle < 1e-3
364
+
365
+ k1_small = 0.5 - angle**2 / 24 + angle**4 / 720
366
+ # Avoid division by zero for non-branching computations. The value will get
367
+ # discarded in the xp.where selection.
368
+ safe_angle = angle + xp.asarray(small_scale, dtype=dtype, device=device)
369
+ k1 = (1.0 - xp.cos(angle)) / safe_angle**2
370
+ k1 = xp.where(small_scale, k1_small, k1)
371
+
372
+ k2_small = 1 / 6 - angle**2 / 120 + angle**4 / 5040
373
+ # Again, avoid division by zero by adding one to all near-zero angles.
374
+ safe_angle = angle + xp.asarray(small_scale, dtype=dtype, device=device)
375
+ k2 = (angle - xp.sin(angle)) / safe_angle**3
376
+ k2 = xp.where(small_scale, k2_small, k2)
377
+
378
+ s = _create_skew_matrix(rot_vec)
379
+ eye = xp.eye(3, dtype=dtype, device=device)
380
+
381
+ return eye + k1[..., None] * s + k2[..., None] * s @ s
382
+
383
+
384
+ def _compute_se3_log_translation_transform(rot_vec: Array) -> Array:
385
+ """Compute the transformation matrix from SE3 translation to the se3
386
+ translation part.
387
+
388
+ It is the inverse of `_compute_se3_exp_translation_transform` in a closed
389
+ analytical form.
390
+ """
391
+ xp = array_namespace(rot_vec)
392
+ dtype = rot_vec.dtype
393
+ device = xp_device(rot_vec)
394
+ angle = xp_vector_norm(rot_vec, axis=-1, keepdims=True, xp=xp)
395
+ mask = angle < 1e-3
396
+
397
+ k_small = 1 / 12 + angle**2 / 720 + angle**4 / 30240
398
+ safe_angle = angle + xp.asarray(mask, dtype=dtype, device=device)
399
+ k = (1 - 0.5 * angle / xp.tan(0.5 * safe_angle)) / safe_angle**2
400
+ k = xp.where(mask, k_small, k)
401
+
402
+ s = _create_skew_matrix(rot_vec)
403
+
404
+ return xp.eye(3, dtype=dtype, device=device) - 0.5 * s + k[..., None] * s @ s
405
+
406
+
407
+ def _create_skew_matrix(vec: Array) -> Array:
408
+ """Create skew-symmetric (aka cross-product) matrix for stack of vectors."""
409
+ xp = array_namespace(vec)
410
+ result = xp.zeros((*vec.shape[:-1], 3, 3), dtype=vec.dtype, device=xp_device(vec))
411
+ result = xpx.at(result)[..., 0, 1].set(-vec[..., 2])
412
+ result = xpx.at(result)[..., 0, 2].set(vec[..., 1])
413
+ result = xpx.at(result)[..., 1, 0].set(vec[..., 2])
414
+ result = xpx.at(result)[..., 1, 2].set(-vec[..., 0])
415
+ result = xpx.at(result)[..., 2, 0].set(-vec[..., 1])
416
+ result = xpx.at(result)[..., 2, 1].set(vec[..., 0])
417
+ return result
418
+
419
+
420
+ def _normalize_dual_quaternion(
421
+ real_part: Array, dual_part: Array
422
+ ) -> tuple[Array, Array]:
423
+ """Ensure that the dual quaternion has unit norm.
424
+
425
+ The norm is a dual number and must be 1 + 0 * epsilon, which means that
426
+ the real quaternion must have unit norm and the dual quaternion must be
427
+ orthogonal to the real quaternion.
428
+ """
429
+ xp = array_namespace(real_part)
430
+
431
+ real_norm = xp_vector_norm(real_part, axis=-1, keepdims=True, xp=xp)
432
+
433
+ # special case: real quaternion is 0, we set it to identity
434
+ zero_real_mask = real_norm == 0.0
435
+ unit_quat = xp.asarray(
436
+ [0.0, 0.0, 0.0, 1.0], dtype=real_part.dtype, device=xp_device(real_part)
437
+ )
438
+ real_part = xp.where(zero_real_mask, unit_quat, real_part)
439
+ real_norm = xp.where(zero_real_mask, 1.0, real_norm)
440
+
441
+ # 1. ensure unit real quaternion
442
+ real_part = real_part / real_norm
443
+ dual_part = dual_part / real_norm
444
+
445
+ # 2. ensure orthogonality of real and dual quaternion
446
+ dual_part -= xp.sum(real_part * dual_part, axis=-1, keepdims=True) * real_part
447
+
448
+ return real_part, dual_part
v12x/pylib/scipy/spatial/transform/_rotation.py ADDED
The diff for this file is too large to render. See raw diff
 
v12x/pylib/scipy/spatial/transform/_rotation_groups.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from scipy.constants import golden as phi
3
+
4
+
5
+ def icosahedral(cls):
6
+ g1 = tetrahedral(cls).as_quat()
7
+ a = 0.5
8
+ b = 0.5 / phi
9
+ c = phi / 2
10
+ g2 = np.array([[+a, +b, +c, 0],
11
+ [+a, +b, -c, 0],
12
+ [+a, +c, 0, +b],
13
+ [+a, +c, 0, -b],
14
+ [+a, -b, +c, 0],
15
+ [+a, -b, -c, 0],
16
+ [+a, -c, 0, +b],
17
+ [+a, -c, 0, -b],
18
+ [+a, 0, +b, +c],
19
+ [+a, 0, +b, -c],
20
+ [+a, 0, -b, +c],
21
+ [+a, 0, -b, -c],
22
+ [+b, +a, 0, +c],
23
+ [+b, +a, 0, -c],
24
+ [+b, +c, +a, 0],
25
+ [+b, +c, -a, 0],
26
+ [+b, -a, 0, +c],
27
+ [+b, -a, 0, -c],
28
+ [+b, -c, +a, 0],
29
+ [+b, -c, -a, 0],
30
+ [+b, 0, +c, +a],
31
+ [+b, 0, +c, -a],
32
+ [+b, 0, -c, +a],
33
+ [+b, 0, -c, -a],
34
+ [+c, +a, +b, 0],
35
+ [+c, +a, -b, 0],
36
+ [+c, +b, 0, +a],
37
+ [+c, +b, 0, -a],
38
+ [+c, -a, +b, 0],
39
+ [+c, -a, -b, 0],
40
+ [+c, -b, 0, +a],
41
+ [+c, -b, 0, -a],
42
+ [+c, 0, +a, +b],
43
+ [+c, 0, +a, -b],
44
+ [+c, 0, -a, +b],
45
+ [+c, 0, -a, -b],
46
+ [0, +a, +c, +b],
47
+ [0, +a, +c, -b],
48
+ [0, +a, -c, +b],
49
+ [0, +a, -c, -b],
50
+ [0, +b, +a, +c],
51
+ [0, +b, +a, -c],
52
+ [0, +b, -a, +c],
53
+ [0, +b, -a, -c],
54
+ [0, +c, +b, +a],
55
+ [0, +c, +b, -a],
56
+ [0, +c, -b, +a],
57
+ [0, +c, -b, -a]])
58
+ return cls.from_quat(np.concatenate((g1, g2)))
59
+
60
+
61
+ def octahedral(cls):
62
+ g1 = tetrahedral(cls).as_quat()
63
+ c = np.sqrt(2) / 2
64
+ g2 = np.array([[+c, 0, 0, +c],
65
+ [0, +c, 0, +c],
66
+ [0, 0, +c, +c],
67
+ [0, 0, -c, +c],
68
+ [0, -c, 0, +c],
69
+ [-c, 0, 0, +c],
70
+ [0, +c, +c, 0],
71
+ [0, -c, +c, 0],
72
+ [+c, 0, +c, 0],
73
+ [-c, 0, +c, 0],
74
+ [+c, +c, 0, 0],
75
+ [-c, +c, 0, 0]])
76
+ return cls.from_quat(np.concatenate((g1, g2)))
77
+
78
+
79
+ def tetrahedral(cls):
80
+ g1 = np.eye(4)
81
+ c = 0.5
82
+ g2 = np.array([[c, -c, -c, +c],
83
+ [c, -c, +c, +c],
84
+ [c, +c, -c, +c],
85
+ [c, +c, +c, +c],
86
+ [c, -c, -c, -c],
87
+ [c, -c, +c, -c],
88
+ [c, +c, -c, -c],
89
+ [c, +c, +c, -c]])
90
+ return cls.from_quat(np.concatenate((g1, g2)))
91
+
92
+
93
+ def dicyclic(cls, n, axis=2):
94
+ g1 = cyclic(cls, n, axis).as_rotvec()
95
+
96
+ thetas = np.linspace(0, np.pi, n, endpoint=False)
97
+ rv = np.pi * np.vstack([np.zeros(n), np.cos(thetas), np.sin(thetas)]).T
98
+ g2 = np.roll(rv, axis, axis=1)
99
+ return cls.from_rotvec(np.concatenate((g1, g2)))
100
+
101
+
102
+ def cyclic(cls, n, axis=2):
103
+ thetas = np.linspace(0, 2 * np.pi, n, endpoint=False)
104
+ rv = np.vstack([thetas, np.zeros(n), np.zeros(n)]).T
105
+ return cls.from_rotvec(np.roll(rv, axis, axis=1))
106
+
107
+
108
+ def create_group(cls, group, axis='Z'):
109
+ if not isinstance(group, str):
110
+ raise ValueError("`group` argument must be a string")
111
+
112
+ permitted_axes = ['x', 'y', 'z', 'X', 'Y', 'Z']
113
+ if axis not in permitted_axes:
114
+ raise ValueError("`axis` must be one of " + ", ".join(permitted_axes))
115
+
116
+ if group in ['I', 'O', 'T']:
117
+ symbol = group
118
+ order = 1
119
+ elif group[:1] in ['C', 'D'] and group[1:].isdigit():
120
+ symbol = group[:1]
121
+ order = int(group[1:])
122
+ else:
123
+ raise ValueError("`group` must be one of 'I', 'O', 'T', 'Dn', 'Cn'")
124
+
125
+ if order < 1:
126
+ raise ValueError("Group order must be positive")
127
+
128
+ axis = 'xyz'.index(axis.lower())
129
+ if symbol == 'I':
130
+ return icosahedral(cls)
131
+ elif symbol == 'O':
132
+ return octahedral(cls)
133
+ elif symbol == 'T':
134
+ return tetrahedral(cls)
135
+ elif symbol == 'D':
136
+ return dicyclic(cls, order, axis=axis)
137
+ elif symbol == 'C':
138
+ return cyclic(cls, order, axis=axis)
139
+ else:
140
+ assert False
v12x/pylib/scipy/spatial/transform/_rotation_spline.py ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from scipy.linalg import solve_banded
3
+ from ._rotation import Rotation
4
+
5
+
6
+ def _create_skew_matrix(x):
7
+ """Create skew-symmetric matrices corresponding to vectors.
8
+
9
+ Parameters
10
+ ----------
11
+ x : ndarray, shape (n, 3)
12
+ Set of vectors.
13
+
14
+ Returns
15
+ -------
16
+ ndarray, shape (n, 3, 3)
17
+ """
18
+ result = np.zeros((len(x), 3, 3))
19
+ result[:, 0, 1] = -x[:, 2]
20
+ result[:, 0, 2] = x[:, 1]
21
+ result[:, 1, 0] = x[:, 2]
22
+ result[:, 1, 2] = -x[:, 0]
23
+ result[:, 2, 0] = -x[:, 1]
24
+ result[:, 2, 1] = x[:, 0]
25
+ return result
26
+
27
+
28
+ def _matrix_vector_product_of_stacks(A, b):
29
+ """Compute the product of stack of matrices and vectors."""
30
+ return np.einsum("ijk,ik->ij", A, b)
31
+
32
+
33
+ def _angular_rate_to_rotvec_dot_matrix(rotvecs):
34
+ """Compute matrices to transform angular rates to rot. vector derivatives.
35
+
36
+ The matrices depend on the current attitude represented as a rotation
37
+ vector.
38
+
39
+ Parameters
40
+ ----------
41
+ rotvecs : ndarray, shape (n, 3)
42
+ Set of rotation vectors.
43
+
44
+ Returns
45
+ -------
46
+ ndarray, shape (n, 3, 3)
47
+ """
48
+ norm = np.linalg.norm(rotvecs, axis=1)
49
+ k = np.empty_like(norm)
50
+
51
+ mask = norm > 1e-4
52
+ nm = norm[mask]
53
+ k[mask] = (1 - 0.5 * nm / np.tan(0.5 * nm)) / nm**2
54
+ mask = ~mask
55
+ nm = norm[mask]
56
+ k[mask] = 1/12 + 1/720 * nm**2
57
+
58
+ skew = _create_skew_matrix(rotvecs)
59
+
60
+ result = np.empty((len(rotvecs), 3, 3))
61
+ result[:] = np.identity(3)
62
+ result[:] += 0.5 * skew
63
+ result[:] += k[:, None, None] * np.matmul(skew, skew)
64
+
65
+ return result
66
+
67
+
68
+ def _rotvec_dot_to_angular_rate_matrix(rotvecs):
69
+ """Compute matrices to transform rot. vector derivatives to angular rates.
70
+
71
+ The matrices depend on the current attitude represented as a rotation
72
+ vector.
73
+
74
+ Parameters
75
+ ----------
76
+ rotvecs : ndarray, shape (n, 3)
77
+ Set of rotation vectors.
78
+
79
+ Returns
80
+ -------
81
+ ndarray, shape (n, 3, 3)
82
+ """
83
+ norm = np.linalg.norm(rotvecs, axis=1)
84
+ k1 = np.empty_like(norm)
85
+ k2 = np.empty_like(norm)
86
+
87
+ mask = norm > 1e-4
88
+ nm = norm[mask]
89
+ k1[mask] = (1 - np.cos(nm)) / nm ** 2
90
+ k2[mask] = (nm - np.sin(nm)) / nm ** 3
91
+
92
+ mask = ~mask
93
+ nm = norm[mask]
94
+ k1[mask] = 0.5 - nm ** 2 / 24
95
+ k2[mask] = 1 / 6 - nm ** 2 / 120
96
+
97
+ skew = _create_skew_matrix(rotvecs)
98
+
99
+ result = np.empty((len(rotvecs), 3, 3))
100
+ result[:] = np.identity(3)
101
+ result[:] -= k1[:, None, None] * skew
102
+ result[:] += k2[:, None, None] * np.matmul(skew, skew)
103
+
104
+ return result
105
+
106
+
107
+ def _angular_acceleration_nonlinear_term(rotvecs, rotvecs_dot):
108
+ """Compute the non-linear term in angular acceleration.
109
+
110
+ The angular acceleration contains a quadratic term with respect to
111
+ the derivative of the rotation vector. This function computes that.
112
+
113
+ Parameters
114
+ ----------
115
+ rotvecs : ndarray, shape (n, 3)
116
+ Set of rotation vectors.
117
+ rotvecs_dot : ndarray, shape (n, 3)
118
+ Set of rotation vector derivatives.
119
+
120
+ Returns
121
+ -------
122
+ ndarray, shape (n, 3)
123
+ """
124
+ norm = np.linalg.norm(rotvecs, axis=1)
125
+ dp = np.sum(rotvecs * rotvecs_dot, axis=1)
126
+ cp = np.cross(rotvecs, rotvecs_dot)
127
+ ccp = np.cross(rotvecs, cp)
128
+ dccp = np.cross(rotvecs_dot, cp)
129
+
130
+ k1 = np.empty_like(norm)
131
+ k2 = np.empty_like(norm)
132
+ k3 = np.empty_like(norm)
133
+
134
+ mask = norm > 1e-4
135
+ nm = norm[mask]
136
+ k1[mask] = (-nm * np.sin(nm) - 2 * (np.cos(nm) - 1)) / nm ** 4
137
+ k2[mask] = (-2 * nm + 3 * np.sin(nm) - nm * np.cos(nm)) / nm ** 5
138
+ k3[mask] = (nm - np.sin(nm)) / nm ** 3
139
+
140
+ mask = ~mask
141
+ nm = norm[mask]
142
+ k1[mask] = 1/12 - nm ** 2 / 180
143
+ k2[mask] = -1/60 + nm ** 2 / 12604
144
+ k3[mask] = 1/6 - nm ** 2 / 120
145
+
146
+ dp = dp[:, None]
147
+ k1 = k1[:, None]
148
+ k2 = k2[:, None]
149
+ k3 = k3[:, None]
150
+
151
+ return dp * (k1 * cp + k2 * ccp) + k3 * dccp
152
+
153
+
154
+ def _compute_angular_rate(rotvecs, rotvecs_dot):
155
+ """Compute angular rates given rotation vectors and its derivatives.
156
+
157
+ Parameters
158
+ ----------
159
+ rotvecs : ndarray, shape (n, 3)
160
+ Set of rotation vectors.
161
+ rotvecs_dot : ndarray, shape (n, 3)
162
+ Set of rotation vector derivatives.
163
+
164
+ Returns
165
+ -------
166
+ ndarray, shape (n, 3)
167
+ """
168
+ return _matrix_vector_product_of_stacks(
169
+ _rotvec_dot_to_angular_rate_matrix(rotvecs), rotvecs_dot)
170
+
171
+
172
+ def _compute_angular_acceleration(rotvecs, rotvecs_dot, rotvecs_dot_dot):
173
+ """Compute angular acceleration given rotation vector and its derivatives.
174
+
175
+ Parameters
176
+ ----------
177
+ rotvecs : ndarray, shape (n, 3)
178
+ Set of rotation vectors.
179
+ rotvecs_dot : ndarray, shape (n, 3)
180
+ Set of rotation vector derivatives.
181
+ rotvecs_dot_dot : ndarray, shape (n, 3)
182
+ Set of rotation vector second derivatives.
183
+
184
+ Returns
185
+ -------
186
+ ndarray, shape (n, 3)
187
+ """
188
+ return (_compute_angular_rate(rotvecs, rotvecs_dot_dot) +
189
+ _angular_acceleration_nonlinear_term(rotvecs, rotvecs_dot))
190
+
191
+
192
+ def _create_block_3_diagonal_matrix(A, B, d):
193
+ """Create a 3-diagonal block matrix as banded.
194
+
195
+ The matrix has the following structure:
196
+
197
+ DB...
198
+ ADB..
199
+ .ADB.
200
+ ..ADB
201
+ ...AD
202
+
203
+ The blocks A, B and D are 3-by-3 matrices. The D matrices has the form
204
+ d * I.
205
+
206
+ Parameters
207
+ ----------
208
+ A : ndarray, shape (n, 3, 3)
209
+ Stack of A blocks.
210
+ B : ndarray, shape (n, 3, 3)
211
+ Stack of B blocks.
212
+ d : ndarray, shape (n + 1,)
213
+ Values for diagonal blocks.
214
+
215
+ Returns
216
+ -------
217
+ ndarray, shape (11, 3 * (n + 1))
218
+ Matrix in the banded form as used by `scipy.linalg.solve_banded`.
219
+ """
220
+ ind = np.arange(3)
221
+ ind_blocks = np.arange(len(A))
222
+
223
+ A_i = np.empty_like(A, dtype=int)
224
+ A_i[:] = ind[:, None]
225
+ A_i += 3 * (1 + ind_blocks[:, None, None])
226
+
227
+ A_j = np.empty_like(A, dtype=int)
228
+ A_j[:] = ind
229
+ A_j += 3 * ind_blocks[:, None, None]
230
+
231
+ B_i = np.empty_like(B, dtype=int)
232
+ B_i[:] = ind[:, None]
233
+ B_i += 3 * ind_blocks[:, None, None]
234
+
235
+ B_j = np.empty_like(B, dtype=int)
236
+ B_j[:] = ind
237
+ B_j += 3 * (1 + ind_blocks[:, None, None])
238
+
239
+ diag_i = diag_j = np.arange(3 * len(d))
240
+ i = np.hstack((A_i.ravel(), B_i.ravel(), diag_i))
241
+ j = np.hstack((A_j.ravel(), B_j.ravel(), diag_j))
242
+ values = np.hstack((A.ravel(), B.ravel(), np.repeat(d, 3)))
243
+
244
+ u = 5
245
+ l = 5
246
+ result = np.zeros((u + l + 1, 3 * len(d)))
247
+ result[u + i - j, j] = values
248
+ return result
249
+
250
+
251
+ class RotationSpline:
252
+ """Interpolate rotations with continuous angular rate and acceleration.
253
+
254
+ The rotation vectors between each consecutive orientation are cubic
255
+ functions of time and it is guaranteed that angular rate and acceleration
256
+ are continuous. Such interpolation are analogous to cubic spline
257
+ interpolation.
258
+
259
+ Refer to [1]_ for math and implementation details.
260
+
261
+ Parameters
262
+ ----------
263
+ times : array_like, shape (N,)
264
+ Times of the known rotations. At least 2 times must be specified.
265
+ rotations : `Rotation` instance
266
+ Rotations to perform the interpolation between. Must contain N
267
+ rotations.
268
+
269
+ Methods
270
+ -------
271
+ __call__
272
+
273
+ References
274
+ ----------
275
+ .. [1] `Smooth Attitude Interpolation
276
+ <https://github.com/scipy/scipy/files/2932755/attitude_interpolation.pdf>`_
277
+
278
+ Examples
279
+ --------
280
+ >>> from scipy.spatial.transform import Rotation, RotationSpline
281
+ >>> import numpy as np
282
+
283
+ Define the sequence of times and rotations from the Euler angles:
284
+
285
+ >>> times = [0, 10, 20, 40]
286
+ >>> angles = [[-10, 20, 30], [0, 15, 40], [-30, 45, 30], [20, 45, 90]]
287
+ >>> rotations = Rotation.from_euler('XYZ', angles, degrees=True)
288
+
289
+ Create the interpolator object:
290
+
291
+ >>> spline = RotationSpline(times, rotations)
292
+
293
+ Interpolate the Euler angles, angular rate and acceleration:
294
+
295
+ >>> angular_rate = np.rad2deg(spline(times, 1))
296
+ >>> angular_acceleration = np.rad2deg(spline(times, 2))
297
+ >>> times_plot = np.linspace(times[0], times[-1], 100)
298
+ >>> angles_plot = spline(times_plot).as_euler('XYZ', degrees=True)
299
+ >>> angular_rate_plot = np.rad2deg(spline(times_plot, 1))
300
+ >>> angular_acceleration_plot = np.rad2deg(spline(times_plot, 2))
301
+
302
+ On this plot you see that Euler angles are continuous and smooth:
303
+
304
+ >>> import matplotlib.pyplot as plt
305
+ >>> plt.plot(times_plot, angles_plot)
306
+ >>> plt.plot(times, angles, 'x')
307
+ >>> plt.title("Euler angles")
308
+ >>> plt.show()
309
+
310
+ The angular rate is also smooth:
311
+
312
+ >>> plt.plot(times_plot, angular_rate_plot)
313
+ >>> plt.plot(times, angular_rate, 'x')
314
+ >>> plt.title("Angular rate")
315
+ >>> plt.show()
316
+
317
+ The angular acceleration is continuous, but not smooth. Also note that
318
+ the angular acceleration is not a piecewise-linear function, because
319
+ it is different from the second derivative of the rotation vector (which
320
+ is a piecewise-linear function as in the cubic spline).
321
+
322
+ >>> plt.plot(times_plot, angular_acceleration_plot)
323
+ >>> plt.plot(times, angular_acceleration, 'x')
324
+ >>> plt.title("Angular acceleration")
325
+ >>> plt.show()
326
+ """
327
+ # Parameters for the solver for angular rate.
328
+ MAX_ITER = 10
329
+ TOL = 1e-9
330
+
331
+ def _solve_for_angular_rates(self, dt, angular_rates, rotvecs):
332
+ angular_rate_first = angular_rates[0].copy()
333
+
334
+ A = _angular_rate_to_rotvec_dot_matrix(rotvecs)
335
+ A_inv = _rotvec_dot_to_angular_rate_matrix(rotvecs)
336
+ M = _create_block_3_diagonal_matrix(
337
+ 2 * A_inv[1:-1] / dt[1:-1, None, None],
338
+ 2 * A[1:-1] / dt[1:-1, None, None],
339
+ 4 * (1 / dt[:-1] + 1 / dt[1:]))
340
+
341
+ b0 = 6 * (rotvecs[:-1] * dt[:-1, None] ** -2 +
342
+ rotvecs[1:] * dt[1:, None] ** -2)
343
+ b0[0] -= 2 / dt[0] * A_inv[0].dot(angular_rate_first)
344
+ b0[-1] -= 2 / dt[-1] * A[-1].dot(angular_rates[-1])
345
+
346
+ for iteration in range(self.MAX_ITER):
347
+ rotvecs_dot = _matrix_vector_product_of_stacks(A, angular_rates)
348
+ delta_beta = _angular_acceleration_nonlinear_term(
349
+ rotvecs[:-1], rotvecs_dot[:-1])
350
+ b = b0 - delta_beta
351
+ angular_rates_new = solve_banded((5, 5), M, b.ravel())
352
+ angular_rates_new = angular_rates_new.reshape((-1, 3))
353
+
354
+ delta = np.abs(angular_rates_new - angular_rates[:-1])
355
+ angular_rates[:-1] = angular_rates_new
356
+ if np.all(delta < self.TOL * (1 + np.abs(angular_rates_new))):
357
+ break
358
+
359
+ rotvecs_dot = _matrix_vector_product_of_stacks(A, angular_rates)
360
+ angular_rates = np.vstack((angular_rate_first, angular_rates[:-1]))
361
+
362
+ return angular_rates, rotvecs_dot
363
+
364
+ def __init__(self, times, rotations):
365
+ from scipy.interpolate import PPoly
366
+
367
+ if rotations.single:
368
+ raise ValueError("`rotations` must be a sequence of rotations.")
369
+
370
+ if rotations.as_quat().ndim > 2:
371
+ raise ValueError(
372
+ "Rotations with more than 1 leading dimension are not supported."
373
+ )
374
+
375
+ if len(rotations) == 1:
376
+ raise ValueError("`rotations` must contain at least 2 rotations.")
377
+
378
+ times = np.asarray(times, dtype=float)
379
+ if times.ndim != 1:
380
+ raise ValueError("`times` must be 1-dimensional.")
381
+
382
+ if len(times) != len(rotations):
383
+ raise ValueError("Expected number of rotations to be equal to "
384
+ "number of timestamps given, "
385
+ f"got {len(rotations)} rotations "
386
+ f"and {len(times)} timestamps.")
387
+
388
+ dt = np.diff(times)
389
+ if np.any(dt <= 0):
390
+ raise ValueError("Values in `times` must be in a strictly "
391
+ "increasing order.")
392
+
393
+ rotvecs = (rotations[:-1].inv() * rotations[1:]).as_rotvec()
394
+ angular_rates = rotvecs / dt[:, None]
395
+
396
+ if len(rotations) == 2:
397
+ rotvecs_dot = angular_rates
398
+ else:
399
+ angular_rates, rotvecs_dot = self._solve_for_angular_rates(
400
+ dt, angular_rates, rotvecs)
401
+
402
+ dt = dt[:, None]
403
+ coeff = np.empty((4, len(times) - 1, 3))
404
+ coeff[0] = (-2 * rotvecs + dt * angular_rates
405
+ + dt * rotvecs_dot) / dt ** 3
406
+ coeff[1] = (3 * rotvecs - 2 * dt * angular_rates
407
+ - dt * rotvecs_dot) / dt ** 2
408
+ coeff[2] = angular_rates
409
+ coeff[3] = 0
410
+
411
+ self.times = times
412
+ self.rotations = rotations
413
+ self.interpolator = PPoly(coeff, times)
414
+
415
+ def __call__(self, times, order=0):
416
+ """Compute interpolated values.
417
+
418
+ Parameters
419
+ ----------
420
+ times : float or array_like
421
+ Times of interest.
422
+ order : {0, 1, 2}, optional
423
+ Order of differentiation:
424
+
425
+ * 0 (default) : return Rotation
426
+ * 1 : return the angular rate in rad/sec
427
+ * 2 : return the angular acceleration in rad/sec/sec
428
+
429
+ Returns
430
+ -------
431
+ Interpolated Rotation, angular rate or acceleration.
432
+ """
433
+ if order not in [0, 1, 2]:
434
+ raise ValueError("`order` must be 0, 1 or 2.")
435
+
436
+ times = np.asarray(times, dtype=float)
437
+ if times.ndim > 1:
438
+ raise ValueError("`times` must be at most 1-dimensional.")
439
+
440
+ singe_time = times.ndim == 0
441
+ times = np.atleast_1d(times)
442
+
443
+ rotvecs = self.interpolator(times)
444
+ if order == 0:
445
+ index = np.searchsorted(self.times, times, side='right')
446
+ index -= 1
447
+ index[index < 0] = 0
448
+ n_segments = len(self.times) - 1
449
+ index[index > n_segments - 1] = n_segments - 1
450
+ result = self.rotations[index] * Rotation.from_rotvec(rotvecs)
451
+ elif order == 1:
452
+ rotvecs_dot = self.interpolator(times, 1)
453
+ result = _compute_angular_rate(rotvecs, rotvecs_dot)
454
+ elif order == 2:
455
+ rotvecs_dot = self.interpolator(times, 1)
456
+ rotvecs_dot_dot = self.interpolator(times, 2)
457
+ result = _compute_angular_acceleration(rotvecs, rotvecs_dot,
458
+ rotvecs_dot_dot)
459
+ else:
460
+ assert False
461
+
462
+ if singe_time:
463
+ result = result[0]
464
+
465
+ return result
v12x/pylib/scipy/spatial/transform/_rotation_xp.py ADDED
@@ -0,0 +1,1140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Array API backend for the `Rotation` class.
2
+
3
+ This module provides generic, functional implementations of the `Rotation` class methods
4
+ that work with any Array API-compatible backend.
5
+ """
6
+
7
+ # Parts of the implementation are adapted from the cython backend and
8
+ # https://github.com/jax-ml/jax/blob/d695aa4c63ffcebefce52794427c46bad576680c/jax/_src/scipy/spatial/transform.py.
9
+ import re
10
+ import warnings
11
+ from types import EllipsisType
12
+
13
+ import numpy as np
14
+ from scipy._lib._array_api import (
15
+ array_namespace,
16
+ Array,
17
+ ArrayLike,
18
+ is_lazy_array,
19
+ xp_vector_norm,
20
+ xp_result_type,
21
+ xp_promote,
22
+ is_jax,
23
+ )
24
+ from scipy._lib._util import broadcastable
25
+ from scipy._external.array_api_compat import device as xp_device
26
+ from scipy._external.array_api_compat import is_array_api_obj
27
+ import scipy._external.array_api_extra as xpx
28
+
29
+ # mypy: disable-error-code=index
30
+ # mypy: disable-error-code=operator
31
+ # mypy: disable-error-code=union-attr
32
+
33
+ def from_quat(
34
+ quat: Array,
35
+ normalize: bool = True,
36
+ copy: bool = True,
37
+ *,
38
+ scalar_first: bool = False,
39
+ ) -> Array:
40
+ xp = array_namespace(quat)
41
+ if scalar_first:
42
+ quat = xp.roll(quat, -1, axis=-1)
43
+ # Normalize will always copy, so we avoid the extra copy if we normalize
44
+ if copy and not normalize:
45
+ quat = xp.asarray(quat, copy=True)
46
+ if normalize:
47
+ quat = _normalize_quaternion(quat)
48
+ return quat
49
+
50
+
51
+ def from_matrix(matrix: Array, assume_valid: bool = False) -> Array:
52
+ xp = array_namespace(matrix)
53
+ device = xp_device(matrix)
54
+
55
+ if not assume_valid:
56
+ mask = xp.linalg.det(matrix) <= 0
57
+ lazy = is_lazy_array(mask)
58
+ # Only non-lazy backends raise an error for non-positive determinants.
59
+ if not lazy and xp.any(mask):
60
+ ind = int(xp.nonzero(xpx.atleast_nd(mask, ndim=1, xp=xp))[0][0])
61
+ raise ValueError(
62
+ "Non-positive determinant (left-handed or null coordinate frame) in "
63
+ f"rotation matrix {ind}: {matrix[ind, ...]}."
64
+ )
65
+ elif lazy:
66
+ matrix = xp.where(mask[..., None, None], xp.nan, matrix)
67
+
68
+ gramians = matrix @ xp.matrix_transpose(matrix)
69
+ eye = xp.eye(3, dtype=matrix.dtype, device=device)
70
+ is_orthogonal = xp.all(
71
+ xpx.isclose(gramians, eye, atol=1e-12, xp=xp), axis=(-2, -1)
72
+ )
73
+
74
+ if lazy:
75
+ # Lazy backends do not support non-concrete boolean indexing or any form of
76
+ # computation without statically known shapes, so we always compute SVD and
77
+ # use xp.where to select the result.
78
+ U, _, Vt = xp.linalg.svd(matrix, full_matrices=False)
79
+ matrix = xp.where(is_orthogonal[..., None, None], matrix, U @ Vt)
80
+ elif not xp.all(is_orthogonal):
81
+ # For eager frameworks, only compute SVD if needed.
82
+ is_not_orthogonal = ~is_orthogonal
83
+ U, _, Vt = xp.linalg.svd(matrix[is_not_orthogonal], full_matrices=False)
84
+ matrix = xpx.at(matrix)[is_not_orthogonal].set(U @ Vt)
85
+
86
+ return _from_matrix_orthogonal(matrix)
87
+
88
+
89
+ def _from_matrix_orthogonal(matrix: Array) -> Array:
90
+ """Convert known orthogonal rotation matrix to quaternion"""
91
+ xp = array_namespace(matrix)
92
+ device = xp_device(matrix)
93
+
94
+ matrix_trace = matrix[..., 0, 0] + matrix[..., 1, 1] + matrix[..., 2, 2]
95
+ decision = xp.stack(
96
+ [matrix[..., 0, 0], matrix[..., 1, 1], matrix[..., 2, 2], matrix_trace],
97
+ axis=-1,
98
+ )
99
+ choice = xp.argmax(decision, axis=-1, keepdims=True)
100
+ quat = xp.empty((*matrix.shape[:-2], 4), dtype=matrix.dtype, device=device)
101
+
102
+ # The Array API does not support mixing integer indexing with ellipsis, so we cannot
103
+ # follow the same pattern as the cython backend. Instead, we compute each case
104
+ # explicitly and assemble the final result with `xp.where`.
105
+ # TODO: Revisit this implementation if the array API supports mixed integer and
106
+ # ellipsis indexing.
107
+
108
+ # Case 0
109
+ quat_0 = xp.stack(
110
+ [
111
+ 1 - matrix_trace[...] + 2 * matrix[..., 0, 0],
112
+ matrix[..., 1, 0] + matrix[..., 0, 1],
113
+ matrix[..., 2, 0] + matrix[..., 0, 2],
114
+ matrix[..., 2, 1] - matrix[..., 1, 2],
115
+ ],
116
+ axis=-1,
117
+ )
118
+ quat = xp.where(choice == 0, quat_0, quat)
119
+
120
+ # Case 1
121
+ quat_1 = xp.stack(
122
+ [
123
+ matrix[..., 1, 0] + matrix[..., 0, 1],
124
+ 1 - matrix_trace[...] + 2 * matrix[..., 1, 1],
125
+ matrix[..., 2, 1] + matrix[..., 1, 2],
126
+ matrix[..., 0, 2] - matrix[..., 2, 0],
127
+ ],
128
+ axis=-1,
129
+ )
130
+ quat = xp.where(choice == 1, quat_1, quat)
131
+
132
+ # Case 2
133
+ quat_2 = xp.stack(
134
+ [
135
+ matrix[..., 2, 0] + matrix[..., 0, 2],
136
+ matrix[..., 2, 1] + matrix[..., 1, 2],
137
+ 1 - matrix_trace[...] + 2 * matrix[..., 2, 2],
138
+ matrix[..., 1, 0] - matrix[..., 0, 1],
139
+ ],
140
+ axis=-1,
141
+ )
142
+ quat = xp.where(choice == 2, quat_2, quat)
143
+
144
+ # Case 3
145
+ quat_3 = xp.stack(
146
+ [
147
+ matrix[..., 2, 1] - matrix[..., 1, 2],
148
+ matrix[..., 0, 2] - matrix[..., 2, 0],
149
+ matrix[..., 1, 0] - matrix[..., 0, 1],
150
+ 1 + matrix_trace[...],
151
+ ],
152
+ axis=-1,
153
+ )
154
+ quat = xp.where(choice == 3, quat_3, quat)
155
+
156
+ return _normalize_quaternion(quat)
157
+
158
+
159
+ def from_rotvec(rotvec: Array, degrees: bool = False) -> Array:
160
+ xp = array_namespace(rotvec)
161
+ if rotvec.shape[-1] != 3:
162
+ raise ValueError(
163
+ f"Expected `rot_vec` to have shape (..., 3), got {rotvec.shape}"
164
+ )
165
+ rotvec = _deg2rad(rotvec) if degrees else rotvec
166
+
167
+ angle = xp_vector_norm(rotvec, axis=-1, keepdims=True, xp=xp)
168
+ small_angle = angle <= 1e-3
169
+ angle2 = angle**2
170
+ small_scale = 0.5 - angle2 / 48 + angle2**2 / 3840
171
+ # We need to handle the case where angle is 0 to avoid division by zero. We use the
172
+ # value of the Taylor series approximation, but non-branching operations require
173
+ # that we still divide by the angle. Since we do not use the result where the angle
174
+ # is close to 0, this is safe.
175
+ div_angle = angle + xp.asarray(small_angle, dtype=angle.dtype)
176
+ large_scale = xp.sin(angle / 2) / div_angle
177
+ scale = xp.where(small_angle, small_scale, large_scale)
178
+ quat = xp.concat([rotvec * scale, xp.cos(angle / 2)], axis=-1)
179
+ return quat
180
+
181
+
182
+ def from_mrp(mrp: Array) -> Array:
183
+ xp = array_namespace(mrp)
184
+ if mrp.shape[-1] != 3:
185
+ raise ValueError(f"Expected `mrp` to have shape (..., 3), got {mrp.shape}")
186
+ mrp2_plus_1 = xp.linalg.vecdot(mrp, mrp, axis=-1)[..., None] + 1
187
+ q_no_norm = xp.concat([2 * mrp[..., :3], (2 - mrp2_plus_1)], axis=-1)
188
+ quat = q_no_norm / mrp2_plus_1
189
+ return quat
190
+
191
+
192
+ def from_euler(seq: str, angles: Array, degrees: bool = False) -> Array:
193
+ xp = array_namespace(angles)
194
+ num_axes = len(seq)
195
+ if num_axes < 1 or num_axes > 3:
196
+ raise ValueError(
197
+ "Expected axis specification to be a non-empty "
198
+ f"string of up to 3 characters, got {seq}"
199
+ )
200
+
201
+ intrinsic = re.match(r"^[XYZ]{1,3}$", seq) is not None
202
+ extrinsic = re.match(r"^[xyz]{1,3}$", seq) is not None
203
+ if not (intrinsic or extrinsic):
204
+ raise ValueError(
205
+ "Expected axes from `seq` to be from ['x', 'y', "
206
+ f"'z'] or ['X', 'Y', 'Z'], got {seq}"
207
+ )
208
+
209
+ if any(seq[i] == seq[i + 1] for i in range(num_axes - 1)):
210
+ raise ValueError(f"Expected consecutive axes to be different, got {seq}")
211
+
212
+ if degrees:
213
+ angles = _deg2rad(angles)
214
+
215
+ angles = xpx.atleast_nd(angles, ndim=1, xp=xp)
216
+
217
+ if angles.shape[-1] != num_axes:
218
+ raise ValueError(
219
+ "Expected last dimension of `angles` to match number of sequence axes "
220
+ f"specified, got {angles.shape[-1]}."
221
+ )
222
+ axes = [_elementary_basis_index(x) for x in seq.lower()]
223
+ q = _elementary_quat_compose(axes, angles, intrinsic)
224
+ return q
225
+
226
+
227
+ def from_davenport(
228
+ axes: Array, order: str, angles: Array, degrees: bool = False
229
+ ) -> Array:
230
+ xp = array_namespace(axes)
231
+ device = xp_device(axes)
232
+ if order in ["e", "extrinsic"]: # Must be static, cannot be jitted
233
+ extrinsic = True
234
+ elif order in ["i", "intrinsic"]:
235
+ extrinsic = False
236
+ else:
237
+ raise ValueError(
238
+ "order should be 'e'/'extrinsic' for extrinsic sequences or 'i'/"
239
+ f"'intrinsic' for intrinsic sequences, got {order}"
240
+ )
241
+
242
+ if axes.shape[-1] != 3:
243
+ raise ValueError("Axes must be vectors of length 3.")
244
+
245
+ axes = xpx.atleast_nd(axes, ndim=2, xp=xp)
246
+ angles = xpx.atleast_nd(angles, ndim=1, xp=xp)
247
+ num_axes = axes.shape[-2]
248
+ if num_axes < 1 or num_axes > 3:
249
+ raise ValueError(f"Expected up to 3 axes, got {num_axes}")
250
+
251
+ axes = axes / xp_vector_norm(axes, axis=-1, keepdims=True, xp=xp)
252
+
253
+ # Check if axes are orthogonal. Shape checks also work for lazy backends.
254
+ axes_not_orthogonal = xp.zeros(axes.shape[:-2], dtype=xp.bool, device=device)
255
+ if num_axes > 1:
256
+ # Cannot be True yet, so we do not need to use xp.logical_or
257
+ axes_not_orthogonal = axes_not_orthogonal | (
258
+ xp.abs(xp.vecdot(axes[..., 0, :], axes[..., 1, :])) > 1e-7
259
+ )
260
+ if num_axes > 2:
261
+ axes_not_orthogonal = axes_not_orthogonal | (
262
+ xp.abs(xp.vecdot(axes[..., 1, :], axes[..., 2, :])) > 1e-7
263
+ )
264
+ if not is_lazy_array(axes_not_orthogonal) and xp.any(axes_not_orthogonal):
265
+ raise ValueError("Consecutive axes must be orthogonal.")
266
+ else:
267
+ axes = xp.where(axes_not_orthogonal[..., None, None], xp.nan, axes)
268
+
269
+ if degrees:
270
+ angles = _deg2rad(angles)
271
+
272
+ if (
273
+ not broadcastable(axes.shape[:-1], angles.shape)
274
+ or axes.shape[-2] != angles.shape[-1]
275
+ ):
276
+ raise ValueError(
277
+ f"Expected `angles` to match number of axes, got {angles.shape} angles "
278
+ f"and {axes.shape} axes."
279
+ )
280
+
281
+ q_shape = angles.shape[:-1] + (4,)
282
+ q = xp.zeros(q_shape, dtype=angles.dtype, device=xp_device(angles))
283
+ q = xpx.at(q)[..., 3].set(1)
284
+
285
+ for i in range(num_axes):
286
+ qi = from_rotvec(angles[..., i, None] * axes[..., i, :])
287
+ q = compose_quat(qi, q) if extrinsic else compose_quat(q, qi)
288
+ return q
289
+
290
+
291
+ def as_quat(
292
+ quat: Array, canonical: bool = False, *, scalar_first: bool = False
293
+ ) -> Array:
294
+ xp = array_namespace(quat)
295
+ if canonical:
296
+ quat = _quat_canonical(quat)
297
+ if scalar_first:
298
+ quat = xp.roll(quat, 1, axis=-1)
299
+ return quat
300
+
301
+
302
+ def as_matrix(quat: Array) -> Array:
303
+ xp = array_namespace(quat)
304
+ x = quat[..., 0]
305
+ y = quat[..., 1]
306
+ z = quat[..., 2]
307
+ w = quat[..., 3]
308
+
309
+ x2 = x * x
310
+ y2 = y * y
311
+ z2 = z * z
312
+ w2 = w * w
313
+
314
+ xy = x * y
315
+ zw = z * w
316
+ xz = x * z
317
+ yw = y * w
318
+ yz = y * z
319
+ xw = x * w
320
+
321
+ matrix_elements = [
322
+ x2 - y2 - z2 + w2,
323
+ 2 * (xy - zw),
324
+ 2 * (xz + yw),
325
+ 2 * (xy + zw),
326
+ -x2 + y2 - z2 + w2,
327
+ 2 * (yz - xw),
328
+ 2 * (xz - yw),
329
+ 2 * (yz + xw),
330
+ -x2 - y2 + z2 + w2,
331
+ ]
332
+ matrix = xp.reshape(xp.stack(matrix_elements, axis=-1), (*quat.shape[:-1], 3, 3))
333
+ return matrix
334
+
335
+
336
+ def as_rotvec(quat: Array, degrees: bool = False) -> Array:
337
+ xp = array_namespace(quat)
338
+ quat = _quat_canonical(quat)
339
+ ax_norm = xp_vector_norm(quat[..., :3], axis=-1, keepdims=True, xp=xp)
340
+ angle = 2 * xp.atan2(ax_norm, quat[..., 3][..., None])
341
+ small_angle = angle <= 1e-3
342
+ angle2 = angle**2
343
+ small_scale = 2 + angle2 / 12 + 7 * angle2**2 / 2880
344
+ # We need to handle the case where sin(angle/2) is 0 to avoid division by zero. We
345
+ # use the value of the Taylor series approximation, but non-branching operations
346
+ # require that we still divide by the sin. Since we do not use the result where the
347
+ # angle is close to 0, adding one to the sin where we discard the result is safe.
348
+ div_sin = xp.sin(angle / 2.0) + xp.asarray(small_angle, dtype=angle.dtype)
349
+ large_scale = angle / div_sin
350
+ scale = xp.where(small_angle, small_scale, large_scale)
351
+ if degrees:
352
+ scale = _rad2deg(scale)
353
+ rotvec = scale * quat[..., :3]
354
+ return rotvec
355
+
356
+
357
+ def as_mrp(quat: Array) -> Array:
358
+ xp = array_namespace(quat)
359
+ one = xp.asarray(1.0, device=xp_device(quat), dtype=quat.dtype)
360
+ sign = xp.where(quat[..., 3, None] < 0, -1, one)
361
+ denominator = 1.0 + sign * quat[..., 3, None]
362
+ return sign * quat[..., :3] / denominator
363
+
364
+
365
+ def as_euler(
366
+ quat: Array, seq: str, degrees: bool = False, *, suppress_warnings: bool = False
367
+ ) -> Array:
368
+ xp = array_namespace(quat)
369
+
370
+ # Sanitize the sequence
371
+ if len(seq) != 3:
372
+ raise ValueError(f"Expected 3 axes, got {seq}.")
373
+ intrinsic = re.match(r"^[XYZ]{1,3}$", seq) is not None
374
+ extrinsic = re.match(r"^[xyz]{1,3}$", seq) is not None
375
+ if not (intrinsic or extrinsic):
376
+ raise ValueError(
377
+ "Expected axes from `seq` to be from ['x', 'y', 'z'] or ['X', 'Y', 'Z'], "
378
+ f"got {seq}"
379
+ )
380
+ if any(seq[i] == seq[i + 1] for i in range(2)):
381
+ raise ValueError(f"Expected consecutive axes to be different, got {seq}")
382
+
383
+ device = xp_device(quat)
384
+ axes = [_elementary_basis_index(x) for x in seq.lower()]
385
+ axes = axes if extrinsic else axes[::-1]
386
+ i, j, k = axes
387
+ symmetric = i == k
388
+ k = 3 - i - j if symmetric else k
389
+
390
+ mask = xp.asarray(symmetric, device=device)
391
+ sign = xp.asarray((i - j) * (j - k) * (k - i) // 2, dtype=quat.dtype, device=device)
392
+ # Permute quaternion elements
393
+ a = xp.where(mask, quat[..., 3], quat[..., 3] - quat[..., j])
394
+ b = xp.where(mask, quat[..., i], quat[..., i] + quat[..., k] * sign)
395
+ c = xp.where(mask, quat[..., j], quat[..., j] + quat[..., 3])
396
+ d = xp.where(mask, quat[..., k] * sign, quat[..., k] * sign - quat[..., i])
397
+
398
+ angles = _get_angles(
399
+ extrinsic, symmetric, sign, xp.pi / 2, a, b, c, d, suppress_warnings
400
+ )
401
+ return _rad2deg(angles) if degrees else angles
402
+
403
+
404
+ def as_davenport(
405
+ quat: Array,
406
+ axes: Array,
407
+ order: str,
408
+ degrees: bool = False,
409
+ *,
410
+ suppress_warnings: bool = False,
411
+ ) -> Array:
412
+ xp = array_namespace(quat)
413
+
414
+ # Check argument validity
415
+ if order in ["e", "extrinsic"]:
416
+ extrinsic = True
417
+ elif order in ["i", "intrinsic"]:
418
+ extrinsic = False
419
+ else:
420
+ raise ValueError(
421
+ "order should be 'e'/'extrinsic' for extrinsic sequences or 'i'/'intrinsic'"
422
+ f" for intrinsic sequences, got {order}"
423
+ )
424
+ if axes.shape[-2] != 3:
425
+ raise ValueError(f"Expected 3 axes, got {axes.shape}.")
426
+ if axes.shape[-1] != 3:
427
+ raise ValueError("Axes must be vectors of length 3.")
428
+ if not broadcastable(axes.shape[:-2], quat.shape[:-1]):
429
+ raise ValueError(
430
+ f"Expected `axes` to match number of rotations, got {axes.shape} axes "
431
+ f"and {quat.shape} rotations."
432
+ )
433
+
434
+ # normalize axes
435
+ axes = axes / xp_vector_norm(axes, axis=-1, keepdims=True, xp=xp)
436
+ vdot_ax0_ax1 = xp.vecdot(axes[..., 0, :], axes[..., 1, :])
437
+ vdot_ax1_ax2 = xp.vecdot(axes[..., 1, :], axes[..., 2, :])
438
+ is_invalid = (vdot_ax0_ax1 >= 1e-7) | (vdot_ax1_ax2 >= 1e-7)
439
+ if is_lazy_array(is_invalid):
440
+ axes = xp.where(is_invalid[..., None, None], xp.nan, axes)
441
+ elif xp.any(is_invalid):
442
+ raise ValueError("Consecutive axes must be orthogonal.")
443
+
444
+ angles = _compute_davenport_from_quat(
445
+ quat,
446
+ axes[..., 0, :],
447
+ axes[..., 1, :],
448
+ axes[..., 2, :],
449
+ extrinsic,
450
+ suppress_warnings,
451
+ )
452
+ if degrees:
453
+ angles = _rad2deg(angles)
454
+ return angles
455
+
456
+
457
+ def inv(quat: Array) -> Array:
458
+ return xpx.at(quat)[..., :3].multiply(-1, copy=True)
459
+
460
+
461
+ def magnitude(quat: Array) -> Array:
462
+ xp = array_namespace(quat)
463
+ sin_q = xp_vector_norm(quat[..., :3], axis=-1, xp=xp)
464
+ cos_q = xp.abs(quat[..., 3])
465
+ angles = 2 * xp.atan2(sin_q, cos_q)
466
+ return angles
467
+
468
+
469
+ def approx_equal(
470
+ quat: Array, other: Array, atol: float | None = None, degrees: bool = False
471
+ ) -> Array:
472
+ if atol is None:
473
+ if degrees:
474
+ warnings.warn(
475
+ "atol must be set to use the degrees flag, defaulting to 1e-8 radians.",
476
+ stacklevel=2,
477
+ )
478
+ atol = 1e-8
479
+ elif degrees:
480
+ atol = _deg2rad(atol)
481
+
482
+ if not broadcastable(quat.shape, other.shape):
483
+ raise ValueError(
484
+ f"Expected broadcastable shapes in both rotations, got {quat.shape[:-1]} "
485
+ f"rotations in first and {other.shape[:-1]} rotations in second object."
486
+ )
487
+
488
+ quat_result = compose_quat(other, inv(quat))
489
+ angles = magnitude(quat_result)
490
+ return angles < atol
491
+
492
+
493
+ def mean(
494
+ quat: Array,
495
+ weights: ArrayLike | None = None,
496
+ axis: None | int | tuple[int, ...] = None,
497
+ ) -> Array:
498
+ xp = array_namespace(quat)
499
+ device = xp_device(quat)
500
+ dtype = xp_result_type(quat, force_floating=True, xp=xp)
501
+ if quat.shape[0] == 0:
502
+ raise ValueError("Mean of an empty rotation set is undefined.")
503
+ # Axis logic: For None, we reduce over all axes. For int, we only reduce over that
504
+ # axis. For tuple, we reduce over all specified axes.
505
+ all_axes = tuple(range(quat.ndim - 1))
506
+ if axis is None:
507
+ axis = all_axes
508
+ elif isinstance(axis, int):
509
+ axis = (axis,)
510
+ if not isinstance(axis, tuple):
511
+ raise ValueError("`axis` must be None, int, or tuple of ints.")
512
+ # Ensure all axes are within bounds
513
+ if axis != () and (min(axis) < -(quat.ndim - 1) or max(axis) > (quat.ndim - 2)):
514
+ raise ValueError(
515
+ f"axis {axis} is out of bounds for rotation with shape {quat.shape[:-1]}."
516
+ )
517
+ # Ensure all axes are positive and unique
518
+ axis = tuple(sorted(set(x % (quat.ndim - 1) for x in axis)))
519
+
520
+ lazy = is_lazy_array(quat)
521
+ # Branching code is okay for checks that include meta info such as shapes and types
522
+ quat_expand = quat[..., None, :]
523
+ if weights is None:
524
+ K = xp.matrix_transpose(quat_expand) @ quat_expand
525
+ else:
526
+ weights = xp.asarray(weights, dtype=dtype, device=device)
527
+ neg_weights = weights < 0
528
+ if not lazy and xp.any(neg_weights):
529
+ raise ValueError("`weights` must be non-negative.")
530
+ elif lazy:
531
+ # We cannot check for negative weights because jit code needs to be
532
+ # non-branching. We return NaN instead
533
+ weights = xp.where(neg_weights, xp.nan, weights)
534
+
535
+ if not broadcastable(quat.shape[:-1], weights.shape):
536
+ raise ValueError(
537
+ "Expected `weights` to be broadcastable to rotation shape, got shape "
538
+ f"{weights.shape} for {quat.shape[:-1]} rotations."
539
+ )
540
+
541
+ # Make sure we can transpose quat
542
+ weighted_quat = weights[..., None, None] * quat_expand
543
+ K = xp.matrix_transpose(weighted_quat) @ quat_expand
544
+
545
+ # Move reduction axes to the end
546
+ keep_axes = tuple(i for i in all_axes if i not in axis)
547
+ axes_order = keep_axes + axis
548
+ K_reordered = xp.moveaxis(K, axes_order, all_axes)
549
+ # Reshape to flatten reduction axes
550
+ new_shape = K_reordered.shape[: len(keep_axes)] + (-1, 4, 4)
551
+ K = xp.mean(xp.reshape(K_reordered, new_shape), axis=-3)
552
+ _, v = xp.linalg.eigh(K)
553
+ return v[..., -1]
554
+
555
+
556
+ def reduce(
557
+ quat: Array,
558
+ left: Array | None = None,
559
+ right: Array | None = None,
560
+ ) -> tuple[Array, Array | None, Array | None]:
561
+ if left is None and right is None:
562
+ return quat, None, None
563
+ # DECISION: We cannot have variable number of return arguments for jit compiled
564
+ # functions. We therefore always return the indices, and filter out later.
565
+ # TOOD: Properly support broadcasting.
566
+ xp = array_namespace(quat)
567
+ quat = xpx.atleast_nd(quat, ndim=2, xp=xp)
568
+ if left is None:
569
+ left = xp.ones_like(quat)
570
+ if right is None:
571
+ right = xp.ones_like(quat)
572
+
573
+ # We want to calculate the real components of q = l * p * r. It can
574
+ # be shown that:
575
+ # qs = ls * ps * rs - ls * dot(pv, rv) - ps * dot(lv, rv)
576
+ # - rs * dot(lv, pv) - dot(cross(lv, pv), rv)
577
+ # where ls and lv denote the scalar and vector components of l.
578
+
579
+ p = quat
580
+ ps, pv = _split_rotation(p, xp)
581
+ ls, lv = _split_rotation(left, xp)
582
+ rs, rv = _split_rotation(right, xp)
583
+
584
+ # Compute each term without einsum (not accessible in the Array API)
585
+ # First term: np.einsum("i,j,k", ls, ps, rs)
586
+ term1 = ls[..., :, None, None] * ps[..., None, :, None] * rs[..., None, None, :]
587
+ # Second term: np.einsum('i,jx,kx', ls, pv, rv)
588
+ prv = xp.sum(pv[..., :, None, :] * rv[..., None, :, :], axis=-1)
589
+ term2 = ls[..., :, None, None] * prv[..., None, :, :]
590
+ # Third term: np.einsum('ix,j,kx', lv, ps, rv)
591
+ lrv = xp.sum(lv[..., :, None, :] * rv[..., None, :, :], axis=-1)
592
+ term3 = ps[..., None, :, None] * lrv[..., :, None, :]
593
+ # Fourth term: np.einsum('ix,jx,k', lv, pv, rs)
594
+ lpv = xp.sum(lv[..., :, None, :] * pv[..., None, :, :], axis=-1)
595
+ term4 = rs[..., None, None, :] * lpv[..., :, :, None]
596
+ # Fifth term: np.einsum('xyz,ix,jy,kz', e, lv, pv, rv). We want to avoid expanding
597
+ # the einsum into a 6D tensor to avoid excessive memory usage. Instead, we compute
598
+ # the cross product between lv and pv and then compute the dot product with rv.
599
+ # First compute cross products between lv and pv
600
+ lv_expanded = lv[..., :, None, :]
601
+ pv_expanded = pv[..., None, :, :]
602
+ cross_lp = xp.linalg.cross(lv_expanded, pv_expanded)
603
+ # Then compute dot product with rv
604
+ term5 = xp.sum(cross_lp[..., :, :, None, :] * rv[..., None, None, :, :], axis=-1)
605
+ # Combine all terms with proper shape alignment
606
+ qs = xp.abs(term1 - term2 - term3 - term4 - term5)
607
+ qs = xp.reshape(xp.moveaxis(qs, 1, 0), (qs.shape[1], -1))
608
+
609
+ # Find best indices from scalar components
610
+ max_ind = xp.argmax(xp.reshape(qs, (qs.shape[0], -1)), axis=1)
611
+ left_best = max_ind // rv.shape[0]
612
+ right_best = max_ind % rv.shape[0]
613
+ # Array API limitation: Integer index arrays are only allowed with integer indices
614
+ # TODO: Can we somehow avoid this?
615
+ all_idx = xp.reshape(xp.arange(left.shape[-1]), (1, -1))
616
+ left_idx = xp.reshape(left_best, (-1, 1))
617
+ left = left[left_idx, all_idx]
618
+ right_idx = xp.reshape(right_best, (-1, 1))
619
+ right = right[right_idx, all_idx]
620
+
621
+ # Reduce the rotation using the best indices
622
+ reduced = compose_quat(left, compose_quat(p, right))
623
+
624
+ if left is None:
625
+ left_best = None
626
+ if right is None:
627
+ right_best = None
628
+ return reduced, left_best, right_best
629
+
630
+
631
+ def apply(quat: Array, points: Array, inverse: bool = False) -> Array:
632
+ xp = array_namespace(quat)
633
+ mat = as_matrix(quat)
634
+ # We do not have access to einsum. To avoid broadcasting issues, we add a singleton
635
+ # dimension to the points array and remove it after the operation.
636
+ points = points[..., None]
637
+ if not broadcastable(mat.shape, points.shape):
638
+ raise ValueError(
639
+ f"Cannot broadcast {quat.shape[:-1]} rotations to {points.shape[:-1]} "
640
+ "vectors."
641
+ )
642
+ if inverse:
643
+ # TODO: Replace with .mT once numpy 2.0 is the minimum supported version
644
+ return (xp.matrix_transpose(mat) @ points)[..., 0]
645
+ return (mat @ points)[..., 0]
646
+
647
+
648
+ def setitem(
649
+ quat: Array, value: Array, indexer: int | slice | EllipsisType | None
650
+ ) -> Array:
651
+ return xpx.at(quat)[indexer, ...].set(value)
652
+
653
+
654
+ def align_vectors(
655
+ a: Array, b: Array, weights: Array | None = None, return_sensitivity: bool = False
656
+ ) -> tuple[Array, Array, Array]:
657
+ xp = array_namespace(a)
658
+ # Check input vectors
659
+ dtype = xp_result_type(a, b, force_floating=True, xp=xp)
660
+ a_original = xp.asarray(a, dtype=dtype)
661
+ b_original = xp.asarray(b, dtype=dtype)
662
+ a = xpx.atleast_nd(a_original, ndim=2, xp=xp)
663
+ b = xpx.atleast_nd(b_original, ndim=2, xp=xp)
664
+ if a.shape[-1] != 3:
665
+ raise ValueError(
666
+ f"Expected input `a` to have shape (3,) or (N, 3), got {a_original.shape}"
667
+ )
668
+ if b.shape[-1] != 3:
669
+ raise ValueError(
670
+ f"Expected input `b` to have shape (3,) or (N, 3), got {b_original.shape}"
671
+ )
672
+ if a.shape != b.shape:
673
+ raise ValueError(
674
+ "Expected inputs `a` and `b` to have same shapes"
675
+ f", got {a_original.shape} and {b_original.shape} respectively."
676
+ )
677
+ if a.ndim > 2 or b.ndim > 2: # This function does not support broadcasting
678
+ raise ValueError(
679
+ "Expected inputs `a` and `b` to have shape (3,) or (N, 3), got "
680
+ f"{a_original.shape} and {b_original.shape} respectively."
681
+ )
682
+ N = a.shape[0]
683
+
684
+ # Check weights
685
+ if weights is None:
686
+ weights = xp.ones(N, device=xp_device(a), dtype=a.dtype)
687
+ else:
688
+ weights = xp.asarray(weights, device=xp_device(a), dtype=a.dtype)
689
+ if weights.ndim != 1:
690
+ raise ValueError(
691
+ f"Expected `weights` to be 1 dimensional, got shape {weights.shape}."
692
+ )
693
+ if N > 1 and (weights.shape[0] != N):
694
+ raise ValueError(
695
+ "Expected `weights` to have number of values equal to number of input "
696
+ f"vectors, got {weights.shape[0]} values and {N} vectors."
697
+ )
698
+ # We can only check for negative weights in eager execution models. Lazy
699
+ # backends will return NaNs instead.
700
+ negative_weights = weights < 0
701
+ if not is_lazy_array(negative_weights) and xp.any(negative_weights):
702
+ raise ValueError("`weights` may not contain negative values")
703
+ weights = xp.where(negative_weights, xp.nan, weights)
704
+
705
+ # For the special case of a single vector pair, we use the infinite
706
+ # weight code path
707
+ weight_is_inf = xp.asarray([True]) if N == 1 else weights == xp.inf
708
+ n_inf = xp.sum(xp.astype(weight_is_inf, a.dtype))
709
+ # We can only error out on multiple infinite weights or sensitivity return with
710
+ # infinite weights in eager execution models. Lazy backends will return NaNs.
711
+ if not is_lazy_array(n_inf):
712
+ if n_inf > 1:
713
+ raise ValueError("Only one infinite weight is allowed")
714
+ if n_inf == 1 and return_sensitivity:
715
+ raise ValueError(
716
+ "Cannot return sensitivity matrix with an "
717
+ "infinite weight or one vector pair"
718
+ )
719
+
720
+ weights = xp.where(n_inf > 1, xp.nan, weights)
721
+
722
+ inf_branch = xp.any(weight_is_inf, axis=-1)
723
+ # DECISION: We cannot compute both branches for all frameworks. There are two main
724
+ # reasons:
725
+ # 1. Computing both for eager execution models is expensive.
726
+ # 2. Some operations will fail when running the unused branch because of numerical
727
+ # and algorithmical issues. Numpy e.g. will raise an exception when trying to
728
+ # compute the svd of a matrix with infinite weights. To prevent this, we only
729
+ # compute the branch that is needed. Lazy backends however require us to take the
730
+ # full compute graph. Therefore, we use xp.where for lazy backends and a branching
731
+ # version for eager frameworks.
732
+ #
733
+ # Note that we could also solve this by exploiting the externals of xpx.apply_where.
734
+ # However, we'd have to rely on the implementation details of apply_where, which is
735
+ # something we should avoid.
736
+ # See https://github.com/scipy/scipy/pull/22777#discussion_r2028868364
737
+ if is_lazy_array(inf_branch):
738
+ q_opt, rssd, sensitivity = _align_vectors(a, b, weights)
739
+ q_opt_inf, rssd_inf, sensitivity_inf = _align_vectors_fixed(a, b, weights)
740
+ q_opt = xp.where(inf_branch, q_opt_inf, q_opt)
741
+ rssd = xp.where(inf_branch, rssd_inf, rssd)
742
+ sensitivity = xp.where(inf_branch, sensitivity_inf, sensitivity)
743
+ else:
744
+ if xp.any(inf_branch):
745
+ q_opt, rssd, sensitivity = _align_vectors_fixed(a, b, weights)
746
+ else:
747
+ q_opt, rssd, sensitivity = _align_vectors(a, b, weights)
748
+ return q_opt, rssd, sensitivity
749
+
750
+
751
+ def _align_vectors(a: Array, b: Array, weights: Array) -> tuple[Array, Array, Array]:
752
+ xp = array_namespace(a)
753
+ device = xp_device(a)
754
+ B = (weights[:, None] * a).mT @ b
755
+ u, s, vh = xp.linalg.svd(B)
756
+
757
+ # Correct improper rotation if necessary (as in Kabsch algorithm)
758
+ neg_det = xp.linalg.det(u @ vh) < 0
759
+ s = xpx.at(s)[..., -1].set(xp.where(neg_det, -s[..., -1], s[..., -1]))
760
+ u = xpx.at(u)[..., :, -1].set(xp.where(neg_det, -u[..., :, -1], u[..., :, -1]))
761
+
762
+ C = u @ vh
763
+
764
+ # DECISION: We cannot branch on the condition because jit code needs to be
765
+ # non-branching. Hence, we omit the check for uniqueness
766
+ # (s[1] + s[2] < 1e-16 * s[0])
767
+ ssd = xp.sum(weights * xp.sum(b**2 + a**2, axis=-1), axis=-1) - 2 * xp.sum(
768
+ s, axis=-1
769
+ )
770
+ rssd = xp.sqrt(xp.maximum(ssd, xp.zeros(1, device=device)))[..., 0]
771
+
772
+ # TODO: We currently need to always compute the sensitivity matrix because lazy code
773
+ # needs to be non-branching. We should check if compilers can optimize the where
774
+ # statement (e.g. in jax) and check if we can have an eager version that only
775
+ # evaluates the branch that is needed.
776
+ # See xpx.apply_where, issue: https://github.com/data-apis/array-api-extra/pull/141
777
+ zeta = (s[..., 0] + s[..., 1]) * (s[..., 1] + s[..., 2]) * (s[..., 2] + s[..., 0])
778
+ kappa = s[..., 0] * s[..., 1] + s[..., 1] * s[..., 2] + s[..., 2] * s[..., 0]
779
+ eye = xp.eye(3, dtype=a.dtype, device=device)
780
+ sensitivity = xp.mean(weights) / zeta * (kappa * eye + B @ B.mT)
781
+ q_opt = _from_matrix_orthogonal(C)
782
+ return q_opt, rssd, sensitivity
783
+
784
+
785
+ def _align_vectors_fixed(
786
+ a: Array, b: Array, weights: Array
787
+ ) -> tuple[Array, Array, Array]:
788
+ xp = array_namespace(a)
789
+ device = xp_device(a)
790
+ N = a.shape[0]
791
+ weight_is_inf = xp.asarray([True], device=device) if N == 1 else weights == xp.inf
792
+ # We cannot use boolean masks for indexing because of jax. For the same reason, we
793
+ # also cannot use dynamic slices. As a workaround, we roll the array so that the
794
+ # infinitely weighted vector is at index 0. We then use static slices to get the
795
+ # primary and secondary vectors.
796
+ #
797
+ # Note that argmax fulfils a secondary purpose here:
798
+ # When we trace this function with jax, this function might get executed even if
799
+ # weight_is_inf does not have a single valid entry. Argmax will still give us a
800
+ # valid index which allows us to proceed with the function (the results are
801
+ # discarded anyways), whereas boolean indexing would result in invalid, zero-shaped
802
+ # arrays.
803
+
804
+ inf_idx = xp.argmax(xp.astype(weight_is_inf, xp.uint8))
805
+ # xp.argmax returns an Array, but the specification of xp.roll does not support
806
+ # Arrays as shifts. For lazy execution models we cannot convert to int because this
807
+ # raises a concretization error. However, jax does accept Arrays as shifts which
808
+ # allows jit compiling the function. Hence we do not convert to int for jax.
809
+ # See https://github.com/data-apis/array-api/issues/914#issuecomment-2918959918
810
+ if not is_jax(xp):
811
+ inf_idx = int(inf_idx)
812
+ a_sorted = xp.roll(a, shift=-inf_idx, axis=0)
813
+ b_sorted = xp.roll(b, shift=-inf_idx, axis=0)
814
+ weights_sorted = xp.roll(weights, shift=-inf_idx, axis=0)
815
+
816
+ a_pri = a_sorted[0, ...][None, ...] # Effectively [[0], ...]
817
+ b_pri = b_sorted[0, ...][None, ...]
818
+ a_pri_norm = xp_vector_norm(a_pri, axis=-1, keepdims=True, xp=xp)
819
+ b_pri_norm = xp_vector_norm(b_pri, axis=-1, keepdims=True, xp=xp)
820
+ if not is_lazy_array(a_pri_norm):
821
+ if xp.any(a_pri_norm == 0) or xp.any(b_pri_norm == 0):
822
+ raise ValueError("Cannot align zero length primary vectors")
823
+
824
+ # We cannot error out on zero length vectors. We set the norm to NaN to avoid
825
+ # division by zero and mark the result as invalid.
826
+ a_pri_norm = xpx.at(a_pri_norm)[a_pri_norm == 0].set(xp.nan)
827
+ b_pri_norm = xpx.at(b_pri_norm)[b_pri_norm == 0].set(xp.nan)
828
+
829
+ a_pri = a_pri / a_pri_norm
830
+ b_pri = b_pri / b_pri_norm
831
+
832
+ # We first find the minimum angle rotation between the primary
833
+ # vectors.
834
+ cross = xp.linalg.cross(b_pri[..., 0, :], a_pri[..., 0, :])
835
+ cross_norm = xp_vector_norm(cross, axis=-1, xp=xp)
836
+ theta = xp.atan2(cross_norm, xp.sum(a_pri[..., 0, :] * b_pri[..., 0, :], axis=-1))
837
+ tolerance = 1e-3 # tolerance for small angle approximation (rad)
838
+ q_flip = xp.asarray([0.0, 0.0, 0.0, 1.0], dtype=a_pri.dtype, device=device)
839
+
840
+ # Near pi radians, the Taylor series approximation of x/sin(x) diverges, so for
841
+ # numerical stability we flip pi and then rotate back by the small angle pi - theta
842
+ flip = xp.pi - theta < tolerance
843
+ # For antiparallel vectors, cross = [0, 0, 0] so we need to manually set an
844
+ # arbitrary orthogonal axis of rotation
845
+ i = xp.argmin(xp.abs(a_pri[..., 0, :]))
846
+ # TODO: Array API does not support fancy indexing __setitem__. The code is
847
+ # equivalent to doing the following:
848
+ # r_components = xp.zeros(3)
849
+ # r_components = xpx.at(r_components)[i - 1].set(a_pri[0, i - 2])
850
+ # r_components = xpx.at(r_components)[i - 2].set(-a_pri[0, i - 1])
851
+ r_components = xp.stack(
852
+ [
853
+ xp.where(i == 1, a_pri[0, 2], xp.where(i == 2, -a_pri[0, 1], 0)),
854
+ xp.where(i == 2, a_pri[0, 0], xp.where(i == 0, -a_pri[0, 2], 0)),
855
+ xp.where(i == 0, a_pri[0, 1], xp.where(i == 1, -a_pri[0, 0], 0)),
856
+ ]
857
+ )
858
+ r = xp.where(cross_norm == 0, r_components, cross)
859
+
860
+ q_flip = xp.where(flip, from_rotvec(r / xp_vector_norm(r, xp=xp) * xp.pi), q_flip)
861
+ theta = xp.where(flip, xp.pi - theta, theta)
862
+ cross = xp.where(flip, -cross, cross)
863
+
864
+ # Small angle Taylor series approximation for numerical stability
865
+ theta2 = theta * theta
866
+ small_scale = xp.abs(theta) < tolerance
867
+ r_small_scale = cross * (1 + theta2 / 6 + theta2 * theta2 * 7 / 360)
868
+ # We need to handle the case where theta is 0 to avoid division by zero. We use the
869
+ # value of the Taylor series approximation, but non-branching operations require
870
+ # that we still divide by the angle. Since we do not use the result where the angle
871
+ # is close to 0, this is safe.
872
+ theta = theta + xp.asarray(small_scale, dtype=theta.dtype)
873
+ r_large_scale = cross * theta / xp.sin(theta)
874
+ r = xp.where(small_scale, r_small_scale, r_large_scale)
875
+ q_pri = compose_quat(from_rotvec(r), q_flip)
876
+
877
+ # Case 1): No secondary vectors, q_opt is q_pri -> Immediately done
878
+ # Case 2): Secondary vectors exist
879
+ # We cannot use boolean masks here because of jax
880
+ a_sec = a_sorted[1:, ...]
881
+ b_sec = b_sorted[1:, ...]
882
+ weights_sec = weights_sorted[1:]
883
+
884
+ # We apply the first rotation to the b vectors to align the
885
+ # primary vectors, resulting in vectors c. The secondary
886
+ # vectors must now be rotated about that primary vector to best
887
+ # align c to a.
888
+ c_sec = apply(q_pri, b_sec)
889
+
890
+ # Calculate vector components for the angle calculation. The
891
+ # angle phi to rotate a single 2D vector C to align to 2D
892
+ # vector A in the xy plane can be found with the equation
893
+ # phi = atan2(cross(C, A), dot(C, A))
894
+ # = atan2(|C|*|A|*sin(phi), |C|*|A|*cos(phi))
895
+ # The below equations perform the same operation, but with the
896
+ # 3D rotation restricted to the 2D plane normal to a_pri, where
897
+ # the secondary vectors are projected into that plane. We then
898
+ # find the composite angle from the weighted sum of the
899
+ # axial components in that plane, minimizing the 2D alignment
900
+ # problem.
901
+ sin_term = xp.linalg.vecdot(xp.linalg.cross(c_sec, a_sec), a_pri)
902
+ cos_term = xp.linalg.vecdot(c_sec, a_sec) - (
903
+ xp.linalg.vecdot(c_sec, a_pri) * xp.linalg.vecdot(a_sec, a_pri)
904
+ )
905
+ phi = xp.atan2(xp.sum(weights_sec * sin_term), xp.sum(weights_sec * cos_term))
906
+ q_sec = from_rotvec(phi * a_pri[0, ...])
907
+
908
+ # Compose these to get the optimal rotation
909
+ q_opt = q_pri if N == 1 else compose_quat(q_sec, q_pri)
910
+
911
+ # Calculate the root sum squared distance. We force the error to
912
+ # be zero for the infinite weight vectors since they will align
913
+ # exactly.
914
+ mask = ((N > 1) | (weights[0] == xp.inf)) & weight_is_inf
915
+ # Skip non-infinite weight single vectors pairs, we used the
916
+ # infinite weight code path but don't want to zero that weight
917
+ weights_inf_zero = xpx.at(weights, mask).set(0, copy=True, xp=xp)
918
+ a_est = apply(q_opt, b)
919
+ rssd = xp.sqrt(xp.sum(weights_inf_zero @ (a - a_est) ** 2))
920
+
921
+ mask = xp.any(xp.isnan(weights), axis=-1)
922
+ q_opt = xp.where(mask, xp.nan, q_opt)
923
+ return q_opt, rssd, xp.asarray(xp.nan, device=device)
924
+
925
+
926
+ def pow(quat: Array, n: float | Array) -> Array:
927
+ xp = array_namespace(quat)
928
+ device = xp_device(quat)
929
+ # If n is an array, we sanitize it to a scalar and promote quat and n to
930
+ # the same dtype.
931
+ if is_array_api_obj(n):
932
+ if n.shape == (1,): # pyrefly:ignore[missing-attribute]
933
+ n = n[0] # pyrefly:ignore[bad-index]
934
+ elif n.ndim != 0: # pyrefly:ignore[missing-attribute]
935
+ raise ValueError("Array exponent must be a scalar")
936
+ quat, n = xp_promote(quat, n, force_floating=True, xp=xp)
937
+
938
+ # If n is a lazy array, we cannot take fast paths for special cases.
939
+ if is_lazy_array(n):
940
+ result = from_rotvec(n * as_rotvec(quat)) # general scaling of rotation angle
941
+ # Special cases 0 -> identity, -1 -> inv, 1 -> copy
942
+ identity = xp.zeros((*quat.shape[:-1], 4), dtype=quat.dtype, device=device)
943
+ identity = xpx.at(identity)[..., 3].set(1)
944
+ result = xp.where(n == 0, identity, result)
945
+ result = xp.where(n == -1, inv(quat), result)
946
+ result = xp.where(n == 1, quat, result)
947
+ return result
948
+ if n == 0:
949
+ identity = xp.zeros((*quat.shape[:-1], 4), dtype=quat.dtype, device=device)
950
+ return xpx.at(identity)[..., 3].set(1)
951
+ if n == -1:
952
+ return inv(quat)
953
+ if n == 1:
954
+ return quat
955
+ return from_rotvec(n * as_rotvec(quat))
956
+
957
+
958
+ def _normalize_quaternion(quat: Array) -> Array:
959
+ xp = array_namespace(quat)
960
+ quat_norm = xp_vector_norm(quat, axis=-1, keepdims=True, xp=xp)
961
+ zero_norm = quat_norm == 0
962
+ if is_lazy_array(quat_norm):
963
+ quat = xp.where(zero_norm, xp.nan, quat)
964
+ elif xp.any(zero_norm):
965
+ raise ValueError("Found zero norm quaternions in `quat`.")
966
+ return quat / quat_norm
967
+
968
+
969
+ def _quat_canonical(quat: Array) -> Array:
970
+ xp = array_namespace(quat)
971
+ mask = quat[..., 3] < 0
972
+ zero_w = quat[..., 3] == 0
973
+ mask = mask | (zero_w & (quat[..., 0] < 0))
974
+ zero_wx = zero_w & (quat[..., 0] == 0)
975
+ mask = mask | (zero_wx & (quat[..., 1] < 0))
976
+ zero_wxy = zero_wx & (quat[..., 1] == 0)
977
+ mask = mask | (zero_wxy & (quat[..., 2] < 0))
978
+ return xp.where(mask[..., None], -quat, quat)
979
+
980
+
981
+ def _elementary_basis_index(axis: str) -> int:
982
+ if axis == "x":
983
+ return 0
984
+ elif axis == "y":
985
+ return 1
986
+ elif axis == "z":
987
+ return 2
988
+ raise ValueError(f"Expected axis to be from ['x', 'y', 'z'], got {axis}")
989
+
990
+
991
+ def _compute_davenport_from_quat(
992
+ quat: Array,
993
+ n1: Array,
994
+ n2: Array,
995
+ n3: Array,
996
+ extrinsic: bool,
997
+ suppress_warnings: bool,
998
+ ) -> Array:
999
+ # The algorithm assumes extrinsic frame transformations. The algorithm
1000
+ # in the paper is formulated for rotation quaternions, which are stored
1001
+ # directly by Rotation.
1002
+ # Adapt the algorithm for our case by reversing both axis sequence and
1003
+ # angles for intrinsic rotations when needed
1004
+ xp = array_namespace(quat)
1005
+ n1, n3 = (n1, n3) if extrinsic else (n3, n1)
1006
+
1007
+ n_cross = xp.linalg.cross(n1, n2)
1008
+ lamb = xp.atan2(xp.vecdot(n3, n_cross), xp.vecdot(n3, n1))
1009
+
1010
+ # alternative set of angles compatible with as_euler implementation
1011
+ mask = lamb < 0
1012
+ n2 = xp.where(mask[..., None], -n2, n2)
1013
+ lamb = xp.where(mask, -lamb, lamb)
1014
+ n_cross = xp.where(mask[..., None], -n_cross, n_cross)
1015
+ correct_set = mask
1016
+
1017
+ quat_lamb = xp.concat(
1018
+ (xp.sin(lamb / 2)[..., None] * n2, xp.cos(lamb / 2)[..., None]), axis=-1
1019
+ )
1020
+
1021
+ q_trans = compose_quat(quat_lamb, quat)
1022
+ a = q_trans[..., 3]
1023
+ b = xp.linalg.vecdot(q_trans[..., :3], n1)
1024
+ c = xp.linalg.vecdot(q_trans[..., :3], n2)
1025
+ d = xp.linalg.vecdot(q_trans[..., :3], n_cross)
1026
+
1027
+ angles = _get_angles(extrinsic, False, 1, lamb, a, b, c, d, suppress_warnings)
1028
+ angles = xpx.at(angles)[..., 1].set(
1029
+ xp.where(correct_set, -angles[..., 1], angles[..., 1])
1030
+ )
1031
+
1032
+ return angles
1033
+
1034
+
1035
+ def _elementary_quat_compose(axes: list[int], angles: Array, intrinsic: bool) -> Array:
1036
+ xp = array_namespace(angles)
1037
+ device = xp_device(angles)
1038
+ quat = _make_elementary_quat(axes[0], angles[..., 0], device=device, xp=xp)
1039
+ for i in range(1, len(axes)):
1040
+ ax_quat = _make_elementary_quat(axes[i], angles[..., i], device=device, xp=xp)
1041
+ quat = compose_quat(quat, ax_quat) if intrinsic else compose_quat(ax_quat, quat)
1042
+ return quat
1043
+
1044
+
1045
+ def _make_elementary_quat(axis: int, angle: Array, device, xp) -> Array:
1046
+ quat = xp.zeros((*angle.shape, 4), dtype=angle.dtype, device=device)
1047
+ quat = xpx.at(quat)[..., 3].set(xp.cos(angle / 2.0))
1048
+ quat = xpx.at(quat)[..., axis].set(xp.sin(angle / 2.0))
1049
+ return quat
1050
+
1051
+
1052
+ def _get_angles(
1053
+ extrinsic: bool,
1054
+ symmetric: bool,
1055
+ sign: int,
1056
+ lamb: float,
1057
+ a: Array,
1058
+ b: Array,
1059
+ c: Array,
1060
+ d: Array,
1061
+ suppress_warnings: bool,
1062
+ ) -> Array:
1063
+ xp = array_namespace(a)
1064
+ device = xp_device(a)
1065
+ eps = 1e-7
1066
+ half_sum = xp.atan2(b, a)
1067
+ half_diff = xp.atan2(d, c)
1068
+ # We zero-initialize to automatically cover singular cases where the second angle is
1069
+ # not defined uniquely.
1070
+ angles = xp.zeros((*a.shape, 3), dtype=a.dtype, device=device)
1071
+
1072
+ angles = xpx.at(angles)[..., 1].set(2 * xp.atan2(xp.hypot(c, d), xp.hypot(a, b)))
1073
+
1074
+ angle_first = 0 if extrinsic else 2
1075
+ angle_third = 2 if extrinsic else 0
1076
+
1077
+ # Check if the second angle is close to 0 or pi, causing a singularity.
1078
+ # - Case 0: Second angle is neither close to 0 nor pi.
1079
+ # - Case 1: Second angle is close to 0.
1080
+ # - Case 2: Second angle is close to pi.
1081
+ case1 = xp.abs(angles[..., 1]) <= eps
1082
+ case2 = xp.abs(angles[..., 1] - xp.pi) <= eps
1083
+ case0 = ~(case1 | case2)
1084
+ if not suppress_warnings and not is_lazy_array(case0) and xp.any(~case0):
1085
+ warnings.warn(
1086
+ "Gimbal lock detected. Setting third angle to zero "
1087
+ "since it is not possible to uniquely determine "
1088
+ "all angles.",
1089
+ stacklevel=3,
1090
+ )
1091
+
1092
+ # This writes case1 into a0 where True and case2 everywhere else. This is sound
1093
+ # because we later overwrite any values without singularity with the regular value
1094
+ # of case0. The second angle is covered by default since we zero-initialized the
1095
+ # second dimension.
1096
+ a0 = xp.where(case1, 2 * half_sum, 2 * half_diff * (-1 if extrinsic else 1))
1097
+ angles = xpx.at(angles)[..., 0].set(a0)
1098
+
1099
+ # We overwrite the values of angles without singularities (case0)
1100
+ a1 = xp.where(case0, half_sum - half_diff, angles[..., angle_first])
1101
+ angles = xpx.at(angles)[..., angle_first].set(a1)
1102
+
1103
+ # Same as above but for the third angle. We overwrite the non-singular case0 values
1104
+ a3 = xp.where(case0, half_sum + half_diff, angles[..., angle_third])
1105
+ if not symmetric:
1106
+ a3 = a3 * sign
1107
+ angles = xpx.at(angles)[..., 1].set(angles[..., 1] - lamb)
1108
+ angles = xpx.at(angles)[..., angle_third].set(a3)
1109
+
1110
+ angles = (angles + xp.pi) % (2 * xp.pi) - xp.pi
1111
+ return angles
1112
+
1113
+
1114
+ def compose_quat(p: Array, q: Array) -> Array:
1115
+ xp = array_namespace(p)
1116
+ cross = xp.linalg.cross(p[..., :3], q[..., :3])
1117
+ qx = p[..., 3] * q[..., 0] + q[..., 3] * p[..., 0] + cross[..., 0]
1118
+ qy = p[..., 3] * q[..., 1] + q[..., 3] * p[..., 1] + cross[..., 1]
1119
+ qz = p[..., 3] * q[..., 2] + q[..., 3] * p[..., 2] + cross[..., 2]
1120
+ qw = (
1121
+ p[..., 3] * q[..., 3]
1122
+ - p[..., 0] * q[..., 0]
1123
+ - p[..., 1] * q[..., 1]
1124
+ - p[..., 2] * q[..., 2]
1125
+ )
1126
+ quat = xp.stack([qx, qy, qz, qw], axis=-1)
1127
+ return quat
1128
+
1129
+
1130
+ def _split_rotation(q: Array, xp) -> tuple[Array, Array]:
1131
+ q = xpx.atleast_nd(q, ndim=2, xp=xp)
1132
+ return q[..., -1], q[..., :-1]
1133
+
1134
+
1135
+ def _deg2rad(angles: Array) -> Array:
1136
+ return angles * (np.pi / 180.0)
1137
+
1138
+
1139
+ def _rad2deg(angles: Array) -> Array:
1140
+ return angles * (180.0 / np.pi)
v12x/pylib/scipy/spatial/transform/rotation.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is not meant for public use and will be removed in SciPy v2.0.0.
2
+ # Use the `scipy.spatial` namespace for importing the functions
3
+ # included below.
4
+
5
+ from scipy._lib.deprecation import _sub_module_deprecation
6
+
7
+
8
+ __all__ = [ # noqa: F822
9
+ 'Rotation',
10
+ 'Slerp',
11
+ ]
12
+
13
+
14
+ def __dir__():
15
+ return __all__
16
+
17
+
18
+ def __getattr__(name):
19
+ return _sub_module_deprecation(sub_package="spatial.transform", module="rotation",
20
+ private_modules=["_rotation"], all=__all__,
21
+ attribute=name)
v12x/pylib/scipy/spatial/transform/tests/__init__.py ADDED
File without changes
v12x/pylib/scipy/spatial/transform/tests/test_rigid_transform.py ADDED
@@ -0,0 +1,1531 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pickle
2
+ from itertools import product
3
+
4
+ import pytest
5
+
6
+ import numpy as np
7
+ from scipy.spatial.transform import Rotation, RigidTransform
8
+ from scipy.spatial.transform._rigid_transform import normalize_dual_quaternion
9
+ from scipy._lib._array_api import (
10
+ is_lazy_array,
11
+ xp_vector_norm,
12
+ is_numpy,
13
+ xp_assert_close,
14
+ make_xp_test_case,
15
+ xp_assert_equal,
16
+ xp_promote
17
+ )
18
+ import scipy._external.array_api_extra as xpx
19
+
20
+
21
+ def rotation_to_xp(r: Rotation, xp):
22
+ dtype = xpx.default_dtype(xp)
23
+ return Rotation.from_quat(xp.asarray(r.as_quat(), dtype=dtype))
24
+
25
+
26
+ def rigid_transform_to_xp(r: RigidTransform, xp):
27
+ dtype = xpx.default_dtype(xp)
28
+ return RigidTransform.from_matrix(xp.asarray(r.as_matrix(), dtype=dtype))
29
+
30
+
31
+ @make_xp_test_case((RigidTransform, "as_matrix"))
32
+ def test_repr(xp):
33
+ actual = repr(RigidTransform.from_matrix(xp.eye(4)))
34
+ expected = """\
35
+ RigidTransform.from_matrix(array([[1., 0., 0., 0.],
36
+ [0., 1., 0., 0.],
37
+ [0., 0., 1., 0.],
38
+ [0., 0., 0., 1.]]))"""
39
+ if is_numpy(xp):
40
+ assert actual == expected
41
+ else:
42
+ assert actual.startswith("RigidTransform.from_matrix(")
43
+
44
+ tf = RigidTransform.from_matrix(xp.asarray(RigidTransform.identity(2).as_matrix()))
45
+ actual = repr(tf)
46
+ expected = """\
47
+ RigidTransform.from_matrix(array([[[1., 0., 0., 0.],
48
+ [0., 1., 0., 0.],
49
+ [0., 0., 1., 0.],
50
+ [0., 0., 0., 1.]],
51
+
52
+ [[1., 0., 0., 0.],
53
+ [0., 1., 0., 0.],
54
+ [0., 0., 1., 0.],
55
+ [0., 0., 0., 1.]]]))"""
56
+ if is_numpy(xp):
57
+ assert actual == expected
58
+ else:
59
+ assert actual.startswith("RigidTransform.from_matrix(")
60
+
61
+
62
+ @make_xp_test_case((RigidTransform, "from_rotation"))
63
+ @pytest.mark.parametrize("ndim", range(1, 4))
64
+ def test_from_rotation(xp, ndim: int):
65
+ atol = 1e-12
66
+ rng = np.random.default_rng(0)
67
+ shape = (ndim,) * (ndim - 1) + (4,)
68
+ r = rotation_to_xp(Rotation.from_quat(rng.normal(size=shape)), xp=xp)
69
+ tf = RigidTransform.from_rotation(r)
70
+ xp_assert_close(tf.as_matrix()[..., :3, :3], r.as_matrix(), atol=atol)
71
+ xp_assert_close(tf.as_matrix()[..., :3, 3], xp.zeros(shape[:-1] + (3,)), atol=atol)
72
+ xp_assert_close(tf.as_matrix()[..., 3, :3], xp.zeros(shape[:-1] + (3,)), atol=atol)
73
+ xp_assert_close(tf.as_matrix()[..., 3, 3], xp.ones(shape[:-1]), atol=atol)
74
+ assert tf.single == (ndim == 1)
75
+
76
+
77
+ @make_xp_test_case((RigidTransform, "from_translation"))
78
+ @pytest.mark.parametrize("ndim", range(1, 4))
79
+ def test_from_translation(xp, ndim: int):
80
+ shape = (ndim,) * (ndim - 1)
81
+ t = xp.reshape(xp.arange(ndim ** (ndim-1) * 3), shape + (3,))
82
+ tf = RigidTransform.from_translation(t)
83
+
84
+ expected = xp.tile(xp.eye(4), shape + (1, 1))
85
+ t_float = xp_promote(t, force_floating=True, xp=xp)
86
+ expected = xpx.at(expected)[..., :3, 3].set(t_float)
87
+ xp_assert_close(tf.as_matrix(), expected)
88
+ assert tf.single == (ndim == 1)
89
+
90
+
91
+ def test_from_translation_array_like():
92
+ # Test single translation
93
+ t = [1, 2, 3]
94
+ tf = RigidTransform.from_translation(t)
95
+ tf_expected = RigidTransform.from_translation(np.array(t))
96
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix())
97
+ assert tf.single
98
+
99
+ # Test multiple translations
100
+ t = [[1, 2, 3], [4, 5, 6]]
101
+ tf = RigidTransform.from_translation(t)
102
+ tf_expected = RigidTransform.from_translation(np.array(t))
103
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix())
104
+ assert not tf.single
105
+
106
+
107
+ @make_xp_test_case((RigidTransform, "from_matrix"), (RigidTransform, "as_matrix"))
108
+ @pytest.mark.parametrize("ndim", range(1, 4))
109
+ def test_from_matrix(xp, ndim: int):
110
+ atol = 1e-12
111
+ shape = (ndim,) * (ndim - 1) + (4, 4)
112
+ dtype = xpx.default_dtype(xp)
113
+
114
+ matrix = xp.tile(xp.eye(4), shape[:-2] + (1, 1))
115
+ t = xp.reshape(xp.arange(ndim ** (ndim-1) * 3, dtype=dtype), shape[:-2] + (3,))
116
+ matrix = xpx.at(matrix)[..., :3, 3].set(t)
117
+
118
+ tf = RigidTransform.from_matrix(matrix)
119
+ xp_assert_close(tf.as_matrix(), matrix, atol=atol)
120
+ assert tf.single == (ndim == 1)
121
+
122
+ # Test non-1 determinant
123
+ matrix = xp.tile(xp.eye(4), shape[:-2] + (1, 1))
124
+ matrix = xpx.at(matrix)[..., :3, :3].set(xp.eye(3) * 2.0)
125
+ tf = RigidTransform.from_matrix(matrix)
126
+ expected = xp.tile(xp.eye(4), shape[:-2] + (1, 1))
127
+ xp_assert_close(tf.as_matrix(), expected, atol=atol)
128
+
129
+ # Test non-orthogonal rotation matrix
130
+ matrix = xp.tile(xp.eye(4), shape[:-2] + (1, 1))
131
+ # matrix is equivalent to [[1, 1, 0, 0],
132
+ # [0, 1, 0, 0],
133
+ # [0, 0, 1, 0],
134
+ # [0, 0, 0, 1]]
135
+ matrix = xpx.at(matrix)[..., 0, 1].set(1.0)
136
+ tf = RigidTransform.from_matrix(matrix)
137
+ expected = xp.tile(xp.eye(4), shape[:-2] + (1, 1))
138
+ expected = xpx.at(expected)[..., 0, 0].set(0.894427)
139
+ expected = xpx.at(expected)[..., 0, 1].set(0.447214)
140
+ expected = xpx.at(expected)[..., 1, 0].set(-0.447214)
141
+ expected = xpx.at(expected)[..., 1, 1].set(0.894427)
142
+ xp_assert_close(tf.as_matrix(), expected, atol=1e-6)
143
+
144
+ # Test invalid matrix
145
+ invalid = xp.tile(xp.eye(4), shape[:-2] + (1, 1))
146
+ invalid = xpx.at(invalid)[..., 3, 3].set(2) # Invalid last row
147
+ if is_lazy_array(invalid):
148
+ tf = RigidTransform.from_matrix(invalid)
149
+ assert xp.all(xp.isnan(tf.as_matrix()))
150
+ else:
151
+ with pytest.raises(ValueError):
152
+ RigidTransform.from_matrix(invalid)
153
+
154
+
155
+ def test_from_matrix_array_like():
156
+ # Test single transform matrix
157
+ matrix = [[1, 0, 0, 0],
158
+ [0, 1, 0, 0],
159
+ [0, 0, 1, 0],
160
+ [0, 0, 0, 1]]
161
+ expected = np.eye(4)
162
+ tf = RigidTransform.from_matrix(matrix)
163
+ xp_assert_close(tf.as_matrix(), expected)
164
+ assert tf.single
165
+
166
+ # Test multiple transform matrices
167
+ matrices = [matrix, matrix]
168
+ tf = RigidTransform.from_matrix(matrices)
169
+ for i in range(len(matrices)):
170
+ xp_assert_close(tf.as_matrix()[i, ...], expected)
171
+ assert not tf.single
172
+
173
+
174
+ @make_xp_test_case((RigidTransform, "from_components"))
175
+ @pytest.mark.parametrize("r_ndim", range(1, 3))
176
+ @pytest.mark.parametrize("t_ndim", range(1, 3))
177
+ def test_from_components(xp, r_ndim: int, t_ndim: int):
178
+ atol = 1e-12
179
+ dims = (6, 5, 4, 3) # Common shape
180
+ q_shape = dims[:r_ndim - 1][::-1] + (4,)
181
+ t_shape = dims[:t_ndim - 1][::-1] + (3,)
182
+ tf_shape = np.broadcast_shapes(q_shape[:-1], t_shape[:-1]) + (4, 4)
183
+ rng = np.random.default_rng(0)
184
+
185
+ t = xp.reshape(xp.arange(np.prod(t_shape[:-1]) * 3), t_shape)
186
+ r = rotation_to_xp(Rotation.from_quat(rng.random(size=q_shape)), xp=xp)
187
+ tf = RigidTransform.from_components(t, r)
188
+
189
+ expected = xp.zeros(tf_shape)
190
+ expected = xpx.at(expected)[..., :3, :3].set(r.as_matrix())
191
+ t_float = xp_promote(t, force_floating=True, xp=xp)
192
+ expected = xpx.at(expected)[..., :3, 3].set(t_float)
193
+ expected = xpx.at(expected)[..., 3, 3].set(1)
194
+ xp_assert_close(tf.as_matrix(), expected, atol=atol)
195
+ assert tf.single == (r_ndim == 1 and t_ndim == 1)
196
+
197
+
198
+ def test_from_components_array_like():
199
+ rng = np.random.default_rng(123)
200
+ # Test single rotation and translation
201
+ t = [1, 2, 3]
202
+ r = Rotation.random(rng=rng)
203
+ tf = RigidTransform.from_components(t, r)
204
+ tf_expected = RigidTransform.from_components(np.array(t), r)
205
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12)
206
+ assert tf.single
207
+
208
+ # Test multiple rotations and translations
209
+ t = [[1, 2, 3], [4, 5, 6]]
210
+ r = Rotation.random(len(t), rng=rng)
211
+ tf = RigidTransform.from_components(t, r)
212
+ tf_expected = RigidTransform.from_components(np.array(t), r)
213
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12)
214
+ assert not tf.single
215
+
216
+
217
+ @make_xp_test_case((RigidTransform, "as_components"))
218
+ @pytest.mark.parametrize("ndim", range(1, 4))
219
+ def test_as_components(xp, ndim):
220
+ dtype = xpx.default_dtype(xp)
221
+ atol = 1e-12 if dtype == xp.float64 else 1e-6
222
+ shape = (ndim,) * (ndim - 1)
223
+ rng = np.random.default_rng(123)
224
+ t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype)
225
+ r = rotation_to_xp(Rotation.from_quat(rng.random(shape + (4,))), xp=xp)
226
+ tf = RigidTransform.from_components(t, r)
227
+ new_t, new_r = tf.as_components()
228
+ assert xp.all(new_r.approx_equal(r, atol=atol))
229
+ xp_assert_close(new_t, t, atol=atol)
230
+
231
+
232
+ @make_xp_test_case((RigidTransform, "from_exp_coords"))
233
+ @pytest.mark.parametrize("dim", range(1, 4))
234
+ def test_from_exp_coords(xp, dim: int):
235
+ shape = (dim,) * (dim - 1)
236
+ # example from 3.3 of
237
+ # https://hades.mech.northwestern.edu/images/2/25/MR-v2.pdf
238
+ dtype = xpx.default_dtype(xp)
239
+ angle1 = np.deg2rad(30.0)
240
+ mat = xp.asarray([
241
+ [np.cos(angle1), -np.sin(angle1), 0.0, 1.0],
242
+ [np.sin(angle1), np.cos(angle1), 0.0, 2.0],
243
+ [0.0, 0.0, 1.0, 0.0],
244
+ [0.0, 0.0, 0.0, 1.0]
245
+ ], dtype=dtype)
246
+ mat = xp.tile(mat, shape + (1, 1))
247
+ tf1 = RigidTransform.from_matrix(mat)
248
+ angle2 = np.deg2rad(60.0)
249
+ mat = xp.asarray([
250
+ [np.cos(angle2), -np.sin(angle2), 0.0, 2.0],
251
+ [np.sin(angle2), np.cos(angle2), 0.0, 1.0],
252
+ [0.0, 0.0, 1.0, 0.0],
253
+ [0.0, 0.0, 0.0, 1.0]
254
+ ], dtype=dtype)
255
+ mat = xp.tile(mat, shape + (1, 1))
256
+ tf2 = RigidTransform.from_matrix(mat)
257
+ expected = tf2 * tf1.inv()
258
+ deg2rag = xp.asarray(np.deg2rad(30.0))
259
+ exp_coords = deg2rag * xp.asarray([0.0, 0.0, 1.0, 3.37, -3.37, 0.0])
260
+ exp_coords = xp.tile(exp_coords, shape + (1,))
261
+ actual = RigidTransform.from_exp_coords(exp_coords)
262
+ xp_assert_close(actual.as_matrix(), expected.as_matrix(), atol=1e-2)
263
+
264
+ # test cases generated by comparison to pytransform3d
265
+ exp_coords = xp.asarray([
266
+ [-2.01041204, -0.52983629, 0.65773501,
267
+ 0.10386614, 0.05855009, 0.54959179],
268
+ [-0.22537438, -0.24132627, -2.4747121,
269
+ -0.09158594, 1.88075832, -0.03197204]
270
+ ])
271
+ exp_coords = xp.tile(exp_coords, shape + (1,))
272
+ expected_matrix = xp.asarray([
273
+ [[0.76406621, 0.10504613, -0.63652819, -0.10209961],
274
+ [0.59956454, -0.47987325, 0.64050295, 0.40158789],
275
+ [-0.2381705, -0.87102639, -0.42963687, 0.19637636],
276
+ [0., 0., 0., 1.]],
277
+ [[-0.78446989, 0.61157488, 0.10287448, 1.33330055],
278
+ [-0.58017785, -0.78232107, 0.22664378, 0.52660831],
279
+ [0.21909052, 0.11810973, 0.96852952, -0.02968529],
280
+ [0., 0., 0., 1.]]
281
+ ])
282
+ expected_matrix = xp.tile(expected_matrix, shape + (1, 1))
283
+ xp_assert_close(
284
+ RigidTransform.from_exp_coords(exp_coords).as_matrix(),
285
+ expected_matrix, atol=1e-8)
286
+
287
+ # identity
288
+ expected_matrix = xp.tile(xp.eye(4), shape + (1, 1))
289
+ exp_coords = xp.zeros(shape + (6,), dtype=dtype)
290
+ xp_assert_close(
291
+ RigidTransform.from_exp_coords(exp_coords).as_matrix(),
292
+ expected_matrix, atol=1e-12)
293
+
294
+ # only translation
295
+ expected_matrix = xp.asarray([
296
+ [[1.0, 0.0, 0.0, 3.0],
297
+ [0.0, 1.0, 0.0, -5.4],
298
+ [0.0, 0.0, 1.0, 100.2],
299
+ [0.0, 0.0, 0.0, 1.0]],
300
+ [[1.0, 0.0, 0.0, -3.0],
301
+ [0.0, 1.0, 0.0, 13.3],
302
+ [0.0, 0.0, 1.0, 1.3],
303
+ [0.0, 0.0, 0.0, 1.0]]
304
+ ])
305
+ expected_matrix = xp.tile(expected_matrix, shape + (1, 1, 1))
306
+ exp_coords = xp.asarray([
307
+ [0.0, 0.0, 0.0, 3.0, -5.4, 100.2],
308
+ [0.0, 0.0, 0.0, -3.0, 13.3, 1.3],
309
+ ])
310
+ exp_coords = xp.tile(exp_coords, shape + (1, 1))
311
+ actual = RigidTransform.from_exp_coords(exp_coords)
312
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=1e-12)
313
+
314
+ # only rotation
315
+ angles = xp.asarray([[34, -12, 0.5], [-102, -55, 30]])
316
+ angles = xp.tile(angles, shape + (1, 1))
317
+ rot = Rotation.from_euler('zyx', angles, degrees=True)
318
+ rotvec = rot.as_rotvec()
319
+ expected_matrix = xp.tile(xp.eye(4), shape + (2, 1, 1))
320
+ expected_matrix = xpx.at(expected_matrix)[..., :3, :3].set(rot.as_matrix())
321
+ exp_coords = xp.concat((rotvec, xp.zeros_like(rotvec)), axis=-1)
322
+ actual = RigidTransform.from_exp_coords(exp_coords)
323
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=1e-12)
324
+
325
+
326
+ def test_from_exp_coords_array_like():
327
+ rng = np.random.default_rng(123)
328
+ # Test single transform
329
+ t = np.array([1, 2, 3])
330
+ r = Rotation.random(rng=rng)
331
+ tf_expected = RigidTransform.from_components(t, r)
332
+ exp_coords = tf_expected.as_exp_coords().tolist()
333
+ assert isinstance(exp_coords, list)
334
+ tf = RigidTransform.from_exp_coords(exp_coords)
335
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12)
336
+
337
+ # Test multiple transforms
338
+ t = [[1, 2, 3], [4, 5, 6]]
339
+ r = Rotation.random(len(t), rng=rng)
340
+ tf_expected = RigidTransform.from_components(t, r)
341
+ exp_coords = tf_expected.as_exp_coords().tolist()
342
+ assert isinstance(exp_coords, list)
343
+ tf = RigidTransform.from_exp_coords(exp_coords)
344
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12)
345
+
346
+
347
+ @make_xp_test_case((RigidTransform, "as_exp_coords"))
348
+ @pytest.mark.parametrize("ndim", range(1, 4))
349
+ def test_as_exp_coords(xp, ndim: int):
350
+ shape = (ndim,) * (ndim - 1)
351
+ # identity
352
+ expected = xp.zeros(shape + (6,))
353
+ actual = RigidTransform.from_exp_coords(expected).as_exp_coords()
354
+ xp_assert_close(actual, expected, atol=1e-12)
355
+
356
+ rng = np.random.default_rng(10)
357
+
358
+ # pure rotation
359
+ rot_vec = xp.asarray(rng.normal(scale=0.1, size=shape + (1000, 3)))
360
+ tf = RigidTransform.from_rotation(Rotation.from_rotvec(rot_vec))
361
+ exp_coords = tf.as_exp_coords()
362
+ xp_assert_close(exp_coords[..., :3], rot_vec, atol=1e-12)
363
+ expected = xp.zeros_like(rot_vec)
364
+ xp_assert_close(exp_coords[..., 3:], expected, atol=1e-16)
365
+
366
+ # pure translation
367
+ translation = xp.asarray(rng.normal(scale=100.0, size=shape + (1000, 3)))
368
+ tf = RigidTransform.from_translation(translation)
369
+ exp_coords = tf.as_exp_coords()
370
+ xp_assert_close(exp_coords[..., :3], expected, atol=1e-16)
371
+ xp_assert_close(exp_coords[..., 3:], translation, atol=1e-15)
372
+
373
+
374
+ @make_xp_test_case((RigidTransform, "from_dual_quat"))
375
+ @pytest.mark.parametrize("ndim", range(1, 4))
376
+ def test_from_dual_quat(xp, ndim: int):
377
+ dtype = xpx.default_dtype(xp)
378
+ atol = 1e-12 if dtype == xp.float64 else 1e-7
379
+ shape = (ndim,) * (ndim - 1)
380
+
381
+ # identity
382
+ dq = xp.asarray([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], dtype=dtype)
383
+ dq = xp.tile(dq, shape + (1,))
384
+ expected = xp.tile(xp.eye(4), shape + (1, 1))
385
+ xp_assert_close(RigidTransform.from_dual_quat(dq).as_matrix(), expected, atol=atol)
386
+ dq = xp.asarray([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], dtype=dtype)
387
+ dq = xp.tile(dq, shape + (1,))
388
+ xp_assert_close(RigidTransform.from_dual_quat(dq, scalar_first=True).as_matrix(),
389
+ expected, atol=atol)
390
+
391
+ # only translation
392
+ dq = xp.asarray([0, 0, 0, 1, 0.25, 0.15, -0.7, 0], dtype=dtype)
393
+ dq = xp.tile(dq, shape + (1,))
394
+ actual = RigidTransform.from_dual_quat(dq)
395
+ expected_matrix = xp.asarray([
396
+ [1, 0, 0, 0.5],
397
+ [0, 1, 0, 0.3],
398
+ [0, 0, 1, -1.4],
399
+ [0, 0, 0, 1]
400
+ ])
401
+ expected_matrix = xp.tile(expected_matrix, shape + (1, 1))
402
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=atol)
403
+ dq = xp.asarray([1, 0, 0, 0, 0, 0.25, 0.15, -0.7], dtype=dtype)
404
+ dq = xp.tile(dq, shape + (1,))
405
+ actual = RigidTransform.from_dual_quat(dq, scalar_first=True)
406
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=atol)
407
+
408
+ # only rotation
409
+ angles = xp.asarray([65, -13, 90], dtype=dtype)
410
+ angles = xp.tile(angles, shape + (1,))
411
+ actual_rot = Rotation.from_euler("xyz", angles, degrees=True)
412
+ qrot = actual_rot.as_quat()
413
+ dq = xp.concat((qrot, xp.zeros_like(qrot)), axis=-1)
414
+ actual = RigidTransform.from_dual_quat(dq)
415
+ expected_matrix = xp.tile(xp.eye(4), shape + (1, 1))
416
+ expected_matrix = xpx.at(expected_matrix)[..., :3, :3].set(actual_rot.as_matrix())
417
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=atol)
418
+
419
+ qrot = actual_rot.as_quat(scalar_first=True)
420
+ dq = xp.concat((qrot, xp.zeros_like(qrot)), axis=-1)
421
+ actual = RigidTransform.from_dual_quat(dq, scalar_first=True)
422
+ expected_matrix = xp.tile(xp.eye(4), shape + (1, 1))
423
+ expected_matrix = xpx.at(expected_matrix)[..., :3, :3].set(actual_rot.as_matrix())
424
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=atol)
425
+
426
+ # rotation and translation
427
+ # rtol is set to 1e-7 because xp_assert_close deviates from
428
+ # np.testing.assert_allclose in that it does not automatically default to 1e-7 for
429
+ # floating point inputs.
430
+ # See https://numpy.org/doc/2.2/reference/generated/numpy.testing.assert_allclose.html
431
+ dq = 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
+ dq = xp.tile(dq, shape + (1, 1))
437
+ actual = RigidTransform.from_dual_quat(dq)
438
+ expected_matrix = xp.asarray(
439
+ [[[0.79398752, -0.60213598, -0.08376202, 0.24605262],
440
+ [0.58613113, 0.79477941, -0.15740392, -0.4932833],
441
+ [0.16135089, 0.07588122, 0.98397557, 0.34262676],
442
+ [0., 0., 0., 1.]],
443
+ [[0.50440981, 0.2957028, 0.81125249, 1.20934468],
444
+ [0.08979911, 0.91647262, -0.3898898, -0.70540077],
445
+ [-0.8587822, 0.26951399, 0.43572393, -0.47776265],
446
+ [0., 0., 0., 1.]]])
447
+ expected_matrix = xp.tile(expected_matrix, shape + (1, 1, 1))
448
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=atol, rtol=1e-7)
449
+
450
+ dq = xp.asarray(
451
+ [[0.94508498, 0.0617101, -0.06483886, 0.31432811,
452
+ -0.07743254, 0.04985168, -0.26119618, 0.1691491],
453
+ [0.8450749, 0.19507259, 0.49404931, -0.06091285,
454
+ 0.04174544, 0.65049656, -0.30782513, 0.16566752]])
455
+ dq = xp.tile(dq, shape + (1, 1))
456
+ actual = RigidTransform.from_dual_quat(dq, scalar_first=True)
457
+ xp_assert_close(actual.as_matrix(), expected_matrix, atol=atol, rtol=1e-7)
458
+
459
+ # unnormalized dual quaternions
460
+
461
+ # invalid real quaternion with norm 0
462
+ dq = xp.zeros(shape + (8,))
463
+ actual = RigidTransform.from_dual_quat(dq)
464
+ expected = xp.tile(xp.eye(4), shape + (1, 1))
465
+ xp_assert_close(actual.as_matrix(), expected, atol=atol)
466
+
467
+ # real quaternion with norm != 1
468
+ unnormalized_dual_quat = xp.asarray(
469
+ [-0.2547655, 1.23506123, 0.20230088, 0.24247194, # norm 1.3
470
+ 0.38559628, 0.08184063, 0.1755943, -0.1582222] # orthogonal
471
+ )
472
+ xp_assert_close(xp_vector_norm(unnormalized_dual_quat[:4]), xp.asarray(1.3)[()],
473
+ atol=atol)
474
+ xp_assert_close(xp.vecdot(unnormalized_dual_quat[:4],
475
+ unnormalized_dual_quat[4:])[()],
476
+ xp.asarray(0.0)[()], atol=1e-8)
477
+
478
+ dq = xp.tile(unnormalized_dual_quat, shape + (1,))
479
+ dual_quat = RigidTransform.from_dual_quat(dq).as_dual_quat()
480
+
481
+ expected_ones = xp.ones(shape) if shape != () else xp.asarray(1.0)[()]
482
+ expected_zeros = xp.zeros(shape) if shape != () else xp.asarray(0.0)[()]
483
+ xp_assert_close(xp_vector_norm(dual_quat[..., :4], axis=-1), expected_ones,
484
+ atol=1e-12)
485
+ vecdot = xp.vecdot(dual_quat[..., :4], dual_quat[..., 4:])
486
+ vecdot = vecdot[()] if vecdot.shape == () else vecdot
487
+ xp_assert_close(vecdot, expected_zeros, atol=atol)
488
+
489
+ # real and dual quaternion are not orthogonal
490
+ unnormalized_dual_quat = xp.asarray(
491
+ [0.20824458, 0.75098079, 0.54542913, -0.30849493, # unit norm
492
+ -0.16051025, 0.10742978, 0.21277201, 0.20596935] # not orthogonal
493
+ )
494
+ xp_assert_close(xp_vector_norm(unnormalized_dual_quat[:4]), xp.asarray(1.0)[()],
495
+ atol=atol)
496
+ assert xp.vecdot(unnormalized_dual_quat[:4], unnormalized_dual_quat[4:]) != 0.0
497
+ dq = xp.tile(unnormalized_dual_quat, shape + (1,))
498
+ dual_quat = RigidTransform.from_dual_quat(dq).as_dual_quat()
499
+
500
+ xp_assert_close(xp_vector_norm(dual_quat[..., :4], axis=-1), expected_ones,
501
+ atol=1e-12)
502
+ vecdot = xp.vecdot(dual_quat[..., :4], dual_quat[..., 4:])
503
+ vecdot = vecdot[()] if vecdot.shape == () else vecdot
504
+ xp_assert_close(vecdot, expected_zeros, atol=atol)
505
+
506
+ # invalid real quaternion with norm 0, non-orthogonal dual quaternion
507
+ unnormalized_dual_quat = xp.asarray(
508
+ [0.0, 0.0, 0.0, 0.0, -0.16051025, 0.10742978, 0.21277201, 0.20596935])
509
+ assert xp.vecdot(xp.asarray([0.0, 0, 0, 1]), unnormalized_dual_quat[4:]) != 0.0
510
+ dq = xp.tile(unnormalized_dual_quat, shape + (1,))
511
+ dual_quat = RigidTransform.from_dual_quat(dq).as_dual_quat()
512
+
513
+ xp_assert_close(xp_vector_norm(dual_quat[..., :4], axis=-1), expected_ones,
514
+ atol=1e-12)
515
+ vecdot = xp.vecdot(dual_quat[..., :4], dual_quat[..., 4:])
516
+ vecdot = vecdot[()] if vecdot.shape == () else vecdot
517
+ xp_assert_close(vecdot, expected_zeros, atol=atol)
518
+
519
+ # compensation for precision loss in real quaternion
520
+ rng = np.random.default_rng(1000)
521
+ t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype)
522
+ q = xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype)
523
+ r = Rotation.from_quat(q)
524
+ random_dual_quats = RigidTransform.from_components(t, r).as_dual_quat()
525
+
526
+ # ensure that random quaternions are not normalized
527
+ random_dual_quats = xpx.at(random_dual_quats)[..., :4].add(0.01)
528
+ assert not xp.any(xpx.isclose(xp_vector_norm(random_dual_quats[..., :4], axis=-1),
529
+ 1.0, atol=0.0001))
530
+ dual_quat_norm = RigidTransform.from_dual_quat(
531
+ random_dual_quats).as_dual_quat()
532
+ xp_assert_close(xp_vector_norm(dual_quat_norm[..., :4], axis=-1), expected_ones,
533
+ atol=atol)
534
+
535
+ # compensation for precision loss in dual quaternion, results in violation
536
+ # of orthogonality constraint
537
+ t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype)
538
+ q = xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype)
539
+ r = Rotation.from_quat(q)
540
+ random_dual_quats = RigidTransform.from_components(t, r).as_dual_quat()
541
+
542
+ # ensure that random quaternions are not normalized
543
+ random_dual_quats = xpx.at(random_dual_quats)[..., 4:].add(0.1)
544
+ q_norm = xp.vecdot(random_dual_quats[..., :4], random_dual_quats[..., 4:])
545
+ assert not xp.any(xpx.isclose(q_norm, 0.0, atol=0.0001))
546
+ dual_quat_norm = RigidTransform.from_dual_quat(
547
+ random_dual_quats).as_dual_quat()
548
+ vecdot = xp.vecdot(dual_quat[..., :4], dual_quat[..., 4:])
549
+ vecdot = vecdot[()] if vecdot.shape == () else vecdot
550
+ xp_assert_close(vecdot, expected_zeros, atol=atol)
551
+ xp_assert_close(random_dual_quats[..., :4], dual_quat_norm[..., :4], atol=atol)
552
+
553
+
554
+ def test_from_dual_quat_array_like():
555
+ rng = np.random.default_rng(123)
556
+ # Test single transform
557
+ t = np.array([1, 2, 3])
558
+ r = Rotation.random(rng=rng)
559
+ tf_expected = RigidTransform.from_components(t, r)
560
+ dual_quat = tf_expected.as_dual_quat().tolist()
561
+ assert isinstance(dual_quat, list)
562
+ tf = RigidTransform.from_dual_quat(dual_quat)
563
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12)
564
+
565
+ # Test multiple transforms
566
+ t = [[1, 2, 3], [4, 5, 6]]
567
+ r = Rotation.random(len(t), rng=rng)
568
+ tf_expected = RigidTransform.from_components(t, r)
569
+ dual_quat = tf_expected.as_dual_quat().tolist()
570
+ assert isinstance(dual_quat, list)
571
+ tf = RigidTransform.from_dual_quat(dual_quat)
572
+ xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12)
573
+
574
+
575
+ @make_xp_test_case((RigidTransform, "as_dual_quat"))
576
+ @pytest.mark.parametrize("ndim", range(1, 4))
577
+ def test_as_dual_quat(xp, ndim: int):
578
+ dtype = xpx.default_dtype(xp)
579
+ shape = (ndim,) * (ndim - 1)
580
+ # identity
581
+ expected = xp.asarray([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], dtype=dtype)
582
+ actual = rigid_transform_to_xp(RigidTransform.identity(), xp).as_dual_quat()
583
+ xp_assert_close(actual, expected, atol=1e-12)
584
+
585
+ expected = xp.asarray([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
586
+ tf = rigid_transform_to_xp(RigidTransform.identity(), xp)
587
+ actual = tf.as_dual_quat(scalar_first=True)
588
+ xp_assert_close(actual, expected, atol=1e-12)
589
+
590
+ rng = np.random.default_rng(10)
591
+
592
+ # only rotation
593
+ for _ in range(10):
594
+ q = xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype)
595
+ real_part = Rotation.from_quat(q).as_quat()
596
+ dual_part = xp.zeros_like(real_part)
597
+ expected = xp.concat((real_part, dual_part), axis=-1)
598
+ actual = RigidTransform.from_dual_quat(expected).as_dual_quat()
599
+ # because of double cover:
600
+ actual = actual * xp.sign(actual[..., 0, None])
601
+ expected = expected * xp.sign(expected[..., 0, None])
602
+ xp_assert_close(actual, expected, atol=1e-12)
603
+
604
+ # only translation
605
+ for _ in range(10):
606
+ tf = 0.5 * xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype)
607
+ expected = xp.zeros(shape + (8,), dtype=dtype)
608
+ expected = xpx.at(expected)[..., 3].set(1.0)
609
+ expected = xpx.at(expected)[..., 4:7].set(tf)
610
+ actual = RigidTransform.from_dual_quat(expected).as_dual_quat()
611
+ # because of double cover:
612
+ actual = actual * xp.sign(actual[..., 0, None])
613
+ expected = expected * xp.sign(expected[..., 0, None])
614
+ xp_assert_close(actual, expected, atol=1e-12)
615
+
616
+ # rotation and translation
617
+ for _ in range(10):
618
+ t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype)
619
+ r = Rotation.from_quat(xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype))
620
+ expected = RigidTransform.from_components(t, r).as_dual_quat()
621
+ actual = RigidTransform.from_dual_quat(expected).as_dual_quat()
622
+ # because of double cover:
623
+ actual = actual * xp.sign(actual[..., 0, None])
624
+ expected = expected * xp.sign(expected[..., 0, None])
625
+ xp_assert_close(actual, expected, atol=1e-12)
626
+
627
+
628
+ @make_xp_test_case(
629
+ (RigidTransform, "from_components"), (RigidTransform, "as_components"),
630
+ (RigidTransform, "from_exp_coords"), (RigidTransform, "as_exp_coords"),
631
+ (RigidTransform, "from_matrix"), (RigidTransform, "as_matrix"),
632
+ (RigidTransform, "from_dual_quat"), (RigidTransform, "as_dual_quat")
633
+ )
634
+ @pytest.mark.parametrize("ndim", range(1, 4))
635
+ def test_from_as_internal_consistency(xp, ndim: int):
636
+ dtype = xpx.default_dtype(xp)
637
+ atol = 1e-12
638
+ n = 10
639
+ rng = np.random.default_rng(10)
640
+ shape = (n,) + (ndim,) * (ndim - 1)
641
+
642
+ t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype)
643
+ r = Rotation.from_quat(xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype))
644
+ tf0 = RigidTransform.from_components(t, r)
645
+
646
+ tf1 = RigidTransform.from_components(*tf0.as_components())
647
+ xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol)
648
+
649
+ tf1 = RigidTransform.from_components(tf0.translation, tf0.rotation)
650
+ xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol)
651
+
652
+ tf1 = RigidTransform.from_exp_coords(tf0.as_exp_coords())
653
+ xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol)
654
+
655
+ tf1 = RigidTransform.from_matrix(tf0.as_matrix())
656
+ xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol)
657
+
658
+ tf1 = RigidTransform.from_dual_quat(tf0.as_dual_quat())
659
+ xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol)
660
+
661
+ # exp_coords small rotation
662
+ t = xp.asarray(rng.normal(scale=1000.0, size=shape + (3,)), dtype=dtype)
663
+ rotvec = xp.asarray(rng.normal(scale=1e-10, size=shape + (3,)), dtype=dtype)
664
+ r = Rotation.from_rotvec(rotvec)
665
+ tf0 = RigidTransform.from_components(t, r)
666
+ tf1 = RigidTransform.from_exp_coords(tf0.as_exp_coords())
667
+ xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol)
668
+
669
+
670
+ def test_identity():
671
+ # We do not use xp here because identity always returns numpy arrays
672
+ atol = 1e-12
673
+ # Test single identity
674
+ tf = RigidTransform.identity()
675
+ xp_assert_close(tf.as_matrix(), np.eye(4), atol=atol)
676
+ # Test multiple identities
677
+ tf = RigidTransform.identity(5)
678
+ xp_assert_close(tf.as_matrix(), np.array([np.eye(4)] * 5), atol=atol)
679
+ # Test shape
680
+ tf = RigidTransform.identity(shape=3)
681
+ expected = np.tile(np.eye(4), (3, 1, 1))
682
+ xp_assert_close(tf.as_matrix(), expected, atol=atol)
683
+ tf = RigidTransform.identity(shape=(2, 3))
684
+ expected = np.tile(np.eye(4), (2, 3, 1, 1))
685
+ xp_assert_close(tf.as_matrix(), expected, atol=atol)
686
+ # Test errors
687
+ with pytest.raises(ValueError, match="Only one of `num` and `shape` can be."):
688
+ RigidTransform.identity(10, shape=(2, 3))
689
+ with pytest.raises(TypeError, match="takes from 0 to 1 positional arguments"):
690
+ RigidTransform.identity(None, (-1, 3)) # Shape is kwarg only
691
+ with pytest.raises(ValueError, match="`shape` must be an int or a tuple of ints"):
692
+ RigidTransform.identity(shape="invalid")
693
+
694
+
695
+ @make_xp_test_case((RigidTransform, "apply"))
696
+ def test_apply(xp):
697
+ atol = 1e-12
698
+ # Broadcast shape: (6, 5, 4, 2) ( + (3,) for vectors, + (4,) for rotations)
699
+ vector_shapes = [(), (1,), (2,), (1, 2), (5, 1, 2)]
700
+ tf_shapes = [(), (1,), (2,), (1, 2), (4, 2), (1, 4, 2), (5, 4, 2), (6, 5, 4, 2)]
701
+ rng = np.random.default_rng(123)
702
+
703
+ for tf_shape, v_shape in product(tf_shapes, vector_shapes):
704
+ # Random rotation and translation
705
+ t = xp.asarray(rng.normal(size=tf_shape + (3,)))
706
+ q = xp.asarray(rng.normal(size=tf_shape + (4,)))
707
+ r = Rotation.from_quat(q)
708
+ tf = RigidTransform.from_components(t, r)
709
+
710
+ vecs = xp.asarray(rng.normal(size=v_shape + (3,)))
711
+ expected = t + r.apply(vecs)
712
+ res = tf.apply(vecs)
713
+ assert res.shape == np.broadcast_shapes(tf_shape, v_shape) + (3,)
714
+ xp_assert_close(res, expected, atol=atol)
715
+
716
+
717
+ def test_apply_array_like():
718
+ rng = np.random.default_rng(123)
719
+ # Single vector
720
+ t = np.array([1, 2, 3])
721
+ r = Rotation.random(rng=rng)
722
+ tf = RigidTransform.from_components(t, r)
723
+ vec = [1, 0, 0]
724
+ expected = t + r.apply(vec)
725
+ xp_assert_close(tf.apply(vec), expected, atol=1e-12)
726
+
727
+ # Multiple vectors
728
+ t = np.array([[1, 2, 3], [4, 5, 6]])
729
+ r = Rotation.random(len(t), rng=rng)
730
+ tf = RigidTransform.from_components(t, r)
731
+ vec = [[1, 0, 0], [0, 1, 0]]
732
+ expected = t + r.apply(vec)
733
+ xp_assert_close(tf.apply(vec), expected, atol=1e-12)
734
+
735
+
736
+ def test_apply_matrix_equivalence():
737
+ """Test documented equivalence for single transform:
738
+ `apply(vector) == translation + vector @ rotation.as_matrix().T.`"""
739
+ t = np.array([1.0, 2.0, 3.0])
740
+ r = Rotation.from_rotvec([0, 0, 1])
741
+ tf = RigidTransform.from_components(t, r)
742
+ # Single vector (3,)
743
+ v = np.array([1.0, 0.0, 0.0])
744
+ xp_assert_close(tf.apply(v), t + v @ r.as_matrix().T)
745
+ # Multiple vectors (P, 3)
746
+ arr = np.array([[1, 0, 0], [0, 1, 0]], dtype=float)
747
+ xp_assert_close(tf.apply(arr), t + arr @ r.as_matrix().T)
748
+
749
+
750
+ @make_xp_test_case((RigidTransform, "apply"))
751
+ def test_inverse_apply(xp):
752
+ atol = 1e-12
753
+ # Broadcast shape: (6, 5, 4, 2) ( + (3,) for vectors, + (4,) for rotations)
754
+ vector_shapes = [(), (1,), (2,), (1, 2), (5, 1, 2)]
755
+ tf_shapes = [(), (1,), (2,), (1, 2), (4, 2), (1, 4, 2), (5, 4, 2), (6, 5, 4, 2)]
756
+ rng = np.random.default_rng(123)
757
+
758
+ for tf_shape, v_shape in product(tf_shapes, vector_shapes):
759
+ # Random rotation and translation
760
+ t = xp.asarray(rng.normal(size=tf_shape + (3,)))
761
+ q = xp.asarray(rng.normal(size=tf_shape + (4,)))
762
+ r = Rotation.from_quat(q)
763
+ tf = RigidTransform.from_components(t, r)
764
+
765
+ vecs = xp.asarray(rng.normal(size=v_shape + (3,)))
766
+ expected = tf.inv().apply(vecs)
767
+ res = tf.apply(vecs, inverse=True)
768
+ assert res.shape == np.broadcast_shapes(tf_shape, v_shape) + (3,)
769
+ xp_assert_close(res, expected, atol=atol)
770
+
771
+
772
+ @make_xp_test_case((RigidTransform, "apply"))
773
+ def test_rotation_alone(xp):
774
+ atol = 1e-12
775
+
776
+ r = Rotation.from_euler('z', xp.asarray(90), degrees=True)
777
+ tf = RigidTransform.from_rotation(r)
778
+ vec = xp.asarray([1, 0, 0])
779
+ expected = r.apply(vec)
780
+ xp_assert_close(tf.apply(vec), expected, atol=atol)
781
+
782
+
783
+ @make_xp_test_case((RigidTransform, "apply"))
784
+ def test_translation_alone(xp):
785
+ atol = 1e-12
786
+ t = xp.asarray([1.0, 2, 3])
787
+ tf = RigidTransform.from_translation(t)
788
+ vec = xp.asarray([5.0, 6, 7])
789
+ expected = t + vec
790
+ xp_assert_close(tf.apply(vec), expected, atol=atol)
791
+
792
+
793
+ @make_xp_test_case((RigidTransform, "apply"), (RigidTransform, "__mul__"))
794
+ def test_composition(xp):
795
+ atol = 1e-12
796
+ tf_shapes = [(), (1,), (2,), (1, 2), (4, 2), (5, 4, 2)]
797
+ dtype = xpx.default_dtype(xp)
798
+ rng = np.random.default_rng(123)
799
+
800
+ for tf_shape1, tf_shape2 in product(tf_shapes, repeat=2):
801
+ # Random rotation and translation
802
+ t1 = xp.asarray(rng.normal(size=tf_shape1 + (3,)))
803
+ q1 = xp.asarray(rng.normal(size=tf_shape1 + (4,)))
804
+ r1 = Rotation.from_quat(q1)
805
+ tf1 = RigidTransform.from_components(t1, r1)
806
+
807
+ t2 = xp.asarray(rng.normal(size=tf_shape2 + (3,)))
808
+ q2 = xp.asarray(rng.normal(size=tf_shape2 + (4,)))
809
+ r2 = Rotation.from_quat(q2)
810
+ tf2 = RigidTransform.from_components(t2, r2)
811
+
812
+ composed = tf2 * tf1
813
+ vec = xp.asarray(rng.normal(size=(3,)), dtype=dtype)
814
+ expected = tf2.apply(tf1.apply(vec))
815
+ res = composed.apply(vec)
816
+ assert res.shape == np.broadcast_shapes(tf_shape1, tf_shape2) + (3,)
817
+ xp_assert_close(res, expected, atol=atol)
818
+
819
+ expected = t2 + r2.apply(t1 + r1.apply(vec))
820
+ xp_assert_close(composed.apply(vec), expected, atol=atol)
821
+ assert composed.single == (tf1.single and tf2.single)
822
+
823
+
824
+ @make_xp_test_case((RigidTransform, "__pow__"), (RigidTransform, "__mul__"))
825
+ @pytest.mark.parametrize("ndim", range(1, 4))
826
+ def test_pow(xp, ndim: int):
827
+ dtype = xpx.default_dtype(xp)
828
+ atol = 1e-12 if dtype == xp.float64 else 1e-6
829
+ num = 10
830
+ rng = np.random.default_rng(100)
831
+ shape = (num,) + (ndim,) * (ndim - 1)
832
+ t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype)
833
+ q = xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype)
834
+ r = Rotation.from_quat(q)
835
+ p = RigidTransform.from_components(t, r)
836
+ p_inv = p.inv()
837
+
838
+ # Test the short-cuts and other integers
839
+ for n in [-5, -2, -1, 0, 1, 2, 5]:
840
+ q = p**n
841
+ # Regression test for gh-24436
842
+ assert isinstance(q._matrix, type(p._matrix))
843
+ r = RigidTransform.from_matrix(xp.tile(xp.eye(4), shape + (1, 1)))
844
+ for _ in range(abs(n)):
845
+ if n > 0:
846
+ r = r * p
847
+ else:
848
+ r = r * p_inv
849
+ xp_assert_close(q.as_matrix(), r.as_matrix(), atol=atol)
850
+
851
+ # Test shape preservation of single
852
+ single_tf = RigidTransform.identity()
853
+ assert (single_tf**n).as_matrix().shape == (4, 4)
854
+
855
+ # Test fractional powers
856
+ q = p**0.5
857
+ xp_assert_close((q * q).as_matrix(), p.as_matrix(), atol=atol)
858
+ q = p**-0.5
859
+ xp_assert_close((q * q).as_matrix(), p.inv().as_matrix(), atol=atol)
860
+ q = p** 1.5
861
+ xp_assert_close((q * q).as_matrix(), (p**3).as_matrix(), atol=atol)
862
+ q = p** -1.5
863
+ xp_assert_close((q * q).as_matrix(), (p**-3).as_matrix(), atol=atol)
864
+
865
+ # pow function
866
+ tf = pow(RigidTransform.from_matrix(xp.eye(4)), 2)
867
+ xp_assert_close(tf.as_matrix(), xp.eye(4), atol=atol)
868
+
869
+
870
+ @make_xp_test_case((RigidTransform, "__pow__"))
871
+ @pytest.mark.parametrize("ndim", range(1, 4))
872
+ def test_pow_equivalence_with_rotation(xp, ndim: int):
873
+ atol = 1e-12
874
+ num = 10
875
+ rng = np.random.default_rng(100)
876
+ dtype = xpx.default_dtype(xp)
877
+ shape = (num,) + (ndim,) * (ndim - 1)
878
+
879
+ r = Rotation.from_quat(xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype))
880
+ p = RigidTransform.from_rotation(r)
881
+ for n in [-5, -2, -1.5, -1, -0.5, 0.0, 0.5, 1, 1.5, 2, 5]:
882
+ xp_assert_close((p**n).rotation.as_matrix(), (r**n).as_matrix(), atol=atol)
883
+
884
+
885
+ @make_xp_test_case((RigidTransform, "inv"), (RigidTransform, "__mul__"))
886
+ @pytest.mark.parametrize("ndim", range(1, 4))
887
+ def test_inverse(xp, ndim: int):
888
+ dtype = xpx.default_dtype(xp)
889
+ atol = 1e-12 if dtype == xp.float64 else 1e-6
890
+ rng = np.random.default_rng(100)
891
+ shape = (ndim,) * (ndim - 1)
892
+
893
+ # Test inverse transform
894
+ r = Rotation.from_quat(xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype))
895
+ t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype)
896
+ tf = RigidTransform.from_components(t, r)
897
+
898
+ # Test that tf * tf.inv() equals identity
899
+ tf_inv = tf.inv()
900
+ composed = tf * tf_inv
901
+ expected = xp.tile(xp.eye(4), shape + (1, 1))
902
+ xp_assert_close(composed.as_matrix(), expected, atol=atol)
903
+
904
+
905
+ @make_xp_test_case((RigidTransform, "as_matrix"))
906
+ @pytest.mark.parametrize("ndim", range(1, 4))
907
+ def test_properties(xp, ndim: int):
908
+ atol = 1e-12 if xpx.default_dtype(xp) == xp.float64 else 1e-6
909
+ shape = (ndim,) * (ndim - 1)
910
+ dtype = xpx.default_dtype(xp)
911
+ rng = np.random.default_rng(100)
912
+
913
+ # Test rotation and translation properties
914
+ r = Rotation.from_quat(xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype))
915
+ t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype)
916
+ tf = RigidTransform.from_components(t, r)
917
+
918
+ xp_assert_close(tf.rotation.as_matrix(), r.as_matrix(), atol=atol)
919
+ assert xp.all(tf.rotation.approx_equal(r, atol=atol))
920
+ xp_assert_close(tf.translation, t, atol=atol)
921
+ # Test that we don't return views that would modify the original array
922
+ xpx.at(tf.translation)[..., 0].set(0.0)
923
+ xp_assert_close(tf.translation, t, atol=atol)
924
+ assert tf.single == (shape == ())
925
+
926
+
927
+ @make_xp_test_case((RigidTransform, "__getitem__"))
928
+ def test_indexing(xp):
929
+ atol = 1e-12
930
+
931
+ # Test indexing for multiple transforms
932
+ r = Rotation.from_euler('zyx', xp.asarray([[90, 0, 0], [0, 90, 0]]), degrees=True)
933
+ t = xp.asarray([[1.0, 2, 3], [4, 5, 6]])
934
+ tf = RigidTransform.from_components(t, r)
935
+
936
+ # Test single index
937
+ xp_assert_close(tf[0].as_matrix()[:3, :3], r[0].as_matrix(), atol=atol)
938
+ xp_assert_close(tf[0].as_matrix()[:3, 3], t[0, ...], atol=atol)
939
+
940
+ # Test slice
941
+ tf_slice = tf[0:2]
942
+ xp_assert_close(tf_slice.as_matrix()[:, :3, :3], r[0:2].as_matrix(), atol=atol)
943
+ xp_assert_close(tf_slice.as_matrix()[:, :3, 3], t[0:2, ...], atol=atol)
944
+
945
+ # Test boolean indexing
946
+ tf_masked = tf[xp.asarray([True, True])]
947
+ xp_assert_close(tf_masked.as_matrix()[:, :3, :3], r.as_matrix(), atol=atol)
948
+ xp_assert_close(tf_masked.as_matrix()[:, :3, 3], t, atol=atol)
949
+
950
+ tf_masked = tf[xp.asarray([False, True])]
951
+ xp_assert_close(tf_masked.as_matrix()[:, :3, :3],
952
+ r[xp.asarray([False, True])].as_matrix(), atol=atol)
953
+ xp_assert_close(tf_masked.as_matrix()[:, :3, 3], t[xp.asarray([False, True])],
954
+ atol=atol)
955
+
956
+ tf_masked = tf[xp.asarray([False, False])]
957
+ assert len(tf_masked) == 0
958
+
959
+ # Test integer array indexing
960
+ idx = xp.asarray([0, 1])
961
+ xp_assert_close(tf[idx].as_matrix()[:, :3, :3], r[idx].as_matrix(), atol=atol)
962
+ xp_assert_close(tf[idx].as_matrix()[:, :3, 3], t, atol=atol)
963
+
964
+
965
+ def test_indexing_array_like():
966
+ atol = 1e-12
967
+
968
+ r = Rotation.from_euler('zyx', np.array([[90, 0, 0], [0, 90, 0]]), degrees=True)
969
+ t = np.array([[1.0, 2, 3], [4, 5, 6]])
970
+ tf = RigidTransform.from_components(t, r)
971
+
972
+ tf_masked = tf[[False, True]]
973
+ xp_assert_close(tf_masked.as_matrix()[:, :3, :3], r[[False, True]].as_matrix(),
974
+ atol=atol)
975
+ xp_assert_close(tf_masked.as_matrix()[:, :3, 3], t[[False, True]], atol=atol)
976
+ tf_masked = tf[[False, False]]
977
+ assert len(tf_masked) == 0
978
+
979
+
980
+ @make_xp_test_case((RigidTransform, "concatenate"))
981
+ def test_concatenate(xp):
982
+ atol = 1e-12
983
+
984
+ # Test concatenation of transforms
985
+ t1 = xp.asarray([1, 0, 0])
986
+ r1 = Rotation.from_euler('z', xp.asarray(90), degrees=True)
987
+ tf1 = RigidTransform.from_components(t1, r1)
988
+
989
+ t2 = xp.asarray([0, 1, 0])
990
+ r2 = Rotation.from_euler('x', xp.asarray(90), degrees=True)
991
+ tf2 = RigidTransform.from_components(t2, r2)
992
+
993
+ # Concatenate single transforms
994
+ concatenated1 = RigidTransform.concatenate([tf1, tf2])
995
+ xp_assert_close(concatenated1[0].as_matrix(), tf1.as_matrix(), atol=atol)
996
+ xp_assert_close(concatenated1[1].as_matrix(), tf2.as_matrix(), atol=atol)
997
+
998
+ # Concatenate multiple transforms
999
+ concatenated2 = RigidTransform.concatenate([tf1, concatenated1])
1000
+ xp_assert_close(concatenated2[0].as_matrix(), tf1.as_matrix(), atol=atol)
1001
+ xp_assert_close(concatenated2[1].as_matrix(), tf1.as_matrix(), atol=atol)
1002
+ xp_assert_close(concatenated2[2].as_matrix(), tf2.as_matrix(), atol=atol)
1003
+
1004
+ # Test ND concatenation
1005
+ tf3 = RigidTransform.from_translation(xp.reshape(xp.arange(18), (3, 2, 3)))
1006
+ tf4 = RigidTransform.from_translation(xp.reshape(xp.arange(18) + 18, (3, 2, 3)))
1007
+ concatenated3 = RigidTransform.concatenate([tf3, tf4])
1008
+ xp_assert_close(concatenated3.as_matrix()[:3, ...], tf3.as_matrix(), atol=atol)
1009
+ xp_assert_close(concatenated3.as_matrix()[3:, ...], tf4.as_matrix(), atol=atol)
1010
+
1011
+
1012
+ @make_xp_test_case((RigidTransform, "from_matrix"))
1013
+ def test_input_validation(xp):
1014
+ # Test invalid matrix shapes
1015
+ inputs = [xp.eye(3), xp.zeros((4, 3)), []]
1016
+ for input in inputs:
1017
+ with pytest.raises(ValueError, match="Expected `matrix` to have shape"):
1018
+ RigidTransform.from_matrix(input)
1019
+
1020
+ # Test invalid last row
1021
+ for ndim in range(3):
1022
+ shape = (ndim,) * (ndim - 1)
1023
+ matrix = xp.zeros(shape + (4, 4))
1024
+ matrix = xpx.at(matrix)[...].set(xp.eye(4))
1025
+ matrix = xpx.at(matrix)[..., 3, :].set(xp.asarray([1.0, 0, 0, 1]))
1026
+ if is_lazy_array(matrix):
1027
+ matrix = RigidTransform.from_matrix(matrix).as_matrix()
1028
+ assert xp.all(xp.isnan(matrix[..., 3, :]))
1029
+ else:
1030
+ with pytest.raises(ValueError, match="last row of transformation matrix"):
1031
+ RigidTransform.from_matrix(matrix)
1032
+
1033
+ # Test left handed rotation matrix
1034
+ matrix = xp.eye(4)
1035
+ matrix = xpx.at(matrix)[0, 0].set(-1)
1036
+ if is_lazy_array(matrix):
1037
+ matrix = RigidTransform.from_matrix(matrix).as_matrix()
1038
+ assert xp.all(xp.isnan(matrix[..., :3, :3]))
1039
+ else:
1040
+ with pytest.raises(ValueError, match="Non-positive determinant"):
1041
+ RigidTransform(matrix, normalize=True)
1042
+
1043
+ # Test non-Rotation input
1044
+ with pytest.raises(TypeError,
1045
+ match="Expected `rotation` to be a `Rotation` instance"):
1046
+ RigidTransform.from_rotation(xp.eye(3))
1047
+
1048
+
1049
+ @make_xp_test_case((RigidTransform, "mean"))
1050
+ @pytest.mark.parametrize("ndim", range(1, 4))
1051
+ def test_mean(xp, ndim: int):
1052
+ atol = 1e-12
1053
+ rng = np.random.default_rng(123)
1054
+
1055
+ dtype = xpx.default_dtype(xp)
1056
+ t = xp.asarray(rng.normal(size=(ndim,) * (ndim - 1) + (3,)), dtype=dtype)
1057
+ q = xp.asarray(rng.normal(size=(ndim,) * (ndim - 1) + (4,)), dtype=dtype)
1058
+ r = Rotation.from_quat(q)
1059
+ tf = RigidTransform.from_components(t, r)
1060
+
1061
+ # Unweighted mean
1062
+ axis = tuple(range(t.ndim - 1))
1063
+ t_mean = xp.mean(t, axis=axis)
1064
+ r_mean = r.mean()
1065
+ tf_mean = tf.mean()
1066
+ assert tf_mean.shape == ()
1067
+ xp_assert_close(tf_mean.as_matrix(),
1068
+ RigidTransform.from_components(t_mean, r_mean).as_matrix(),
1069
+ atol=atol)
1070
+
1071
+ # Weighted mean
1072
+ if ndim == 1:
1073
+ weights = None
1074
+ t_mean = t
1075
+ else:
1076
+ weights = xp.asarray(rng.random(size=(ndim,) * (ndim - 1)), dtype=dtype)
1077
+ norm = xp.sum(weights[..., None], axis=axis)
1078
+ wsum = xp.sum(t * weights[..., None], axis=axis)
1079
+ t_mean = wsum/norm
1080
+ r_mean = r.mean(weights=weights)
1081
+ tf_mean = tf.mean(weights=weights)
1082
+ assert tf_mean.shape == ()
1083
+ xp_assert_close(tf_mean.as_matrix(),
1084
+ RigidTransform.from_components(t_mean, r_mean).as_matrix(),
1085
+ atol=atol)
1086
+
1087
+
1088
+ @make_xp_test_case(
1089
+ (RigidTransform, "from_rotation"), (RigidTransform, "mean"), (Rotation, "magnitude")
1090
+ )
1091
+ @pytest.mark.parametrize("ndim", range(1, 5))
1092
+ def test_mean_axis(xp, ndim: int):
1093
+ axes = xp.tile(xp.concat((-xp.eye(3), xp.eye(3))), (3,) * (ndim - 1) + (1, 1))
1094
+ theta = xp.pi / 4
1095
+ r = Rotation.from_rotvec(theta * axes)
1096
+ tf = RigidTransform.from_rotation(r)
1097
+
1098
+ # Test mean over last axis
1099
+ desired = xp.full(axes.shape[:-2], 0.0)
1100
+ if ndim == 1:
1101
+ desired = desired[()]
1102
+ atol = 1e-6 if xpx.default_dtype(xp) is xp.float32 else 1e-10
1103
+ xp_assert_close(tf.mean(axis=-1).rotation.magnitude(), desired, atol=atol)
1104
+
1105
+ # Test tuple axes
1106
+ desired = xp.full(axes.shape[1:-2], 0.0)
1107
+ if ndim < 3:
1108
+ desired = desired[()]
1109
+ xp_assert_close(tf.mean(axis=(0, -1)).rotation.magnitude(), desired, atol=atol)
1110
+
1111
+ # Empty axis tuple should return RigidTransform unchanged
1112
+ tf_mean = tf.mean(axis=())
1113
+ xp_assert_close(tf_mean.as_matrix(), tf.as_matrix(), atol=atol)
1114
+
1115
+
1116
+ @make_xp_test_case((RigidTransform, "mean"), (Rotation, "magnitude"))
1117
+ def test_mean_compare_axis(xp):
1118
+ # Create a random set of transforms and compare the mean over an axis with
1119
+ # the mean without axis of the sliced transform
1120
+ atol = 1e-10 if xpx.default_dtype(xp) == xp.float64 else 1e-6
1121
+ rng = np.random.default_rng(0)
1122
+ q = xp.asarray(rng.normal(size=(4, 5, 6, 4)), dtype=xpx.default_dtype(xp))
1123
+ r = Rotation.from_quat(q)
1124
+ t = xp.asarray(rng.normal(size=(4, 5, 6, 3)), dtype=xpx.default_dtype(xp))
1125
+ tf = RigidTransform.from_components(t, r)
1126
+
1127
+ mean_0 = tf.mean(axis=0)
1128
+ for i in range(q.shape[1]):
1129
+ for j in range(q.shape[2]):
1130
+ r_slice = Rotation.from_quat(q[:, i, j, ...])
1131
+ t_slice = t[:, i, j, ...]
1132
+ mean_slice_tf = RigidTransform.from_components(t_slice, r_slice).mean()
1133
+ xp_assert_close(
1134
+ (mean_0[i][j].rotation * mean_slice_tf.rotation.inv()).magnitude(),
1135
+ xp.asarray(0.0)[()], atol=atol,
1136
+ )
1137
+ xp_assert_close(
1138
+ mean_0[i][j].translation, mean_slice_tf.translation, atol=atol,
1139
+ )
1140
+ mean_1_2 = tf.mean(axis=(1, 2))
1141
+ for i in range(q.shape[0]):
1142
+ r_slice = Rotation.from_quat(q[i, ...])
1143
+ t_slice = t[i, ...]
1144
+ mean_slice_tf = RigidTransform.from_components(t_slice, r_slice).mean()
1145
+ xp_assert_close(
1146
+ (mean_1_2[i].rotation * mean_slice_tf.rotation.inv()).magnitude(),
1147
+ xp.asarray(0.0)[()], atol=atol,
1148
+ )
1149
+ xp_assert_close(
1150
+ mean_1_2[i].translation, mean_slice_tf.translation, atol=atol,
1151
+ )
1152
+
1153
+
1154
+ @make_xp_test_case((RigidTransform, "mean"))
1155
+ def test_mean_invalid_weights(xp):
1156
+ tf = RigidTransform.from_matrix(xp.tile(xp.eye(4), (4, 1, 1)))
1157
+ if is_lazy_array(tf.as_matrix()):
1158
+ m = tf.mean(weights=-xp.ones(4))
1159
+ assert xp.all(xp.isnan(m.as_matrix()))
1160
+ else:
1161
+ with pytest.raises(ValueError, match="non-negative"):
1162
+ tf.mean(weights=-xp.ones(4))
1163
+
1164
+ # Test weight shape mismatch
1165
+ tf = RigidTransform.from_matrix(xp.eye(4))
1166
+ with pytest.raises(ValueError, match="Expected `weights` to"):
1167
+ tf.mean(weights=xp.ones((2,)))
1168
+ tf = RigidTransform.from_matrix(xp.tile(xp.eye(4), (3, 2, 1, 1, 1)))
1169
+ with pytest.raises(ValueError, match="Expected `weights` to"):
1170
+ tf.mean(weights=xp.ones((2, 1)))
1171
+
1172
+
1173
+ @make_xp_test_case((RigidTransform, "from_translation"))
1174
+ def test_translation_validation(xp):
1175
+ # Test invalid translation shapes
1176
+ with pytest.raises(ValueError, match="Expected `translation` to have shape"):
1177
+ RigidTransform.from_translation(xp.asarray([1, 2]))
1178
+
1179
+ with pytest.raises(ValueError, match="Expected `translation` to have shape"):
1180
+ RigidTransform.from_translation(xp.zeros((2, 2)))
1181
+
1182
+
1183
+ @make_xp_test_case((RigidTransform, "apply"))
1184
+ def test_vector_validation(xp):
1185
+ tf = rigid_transform_to_xp(RigidTransform.identity(2), xp=xp)
1186
+
1187
+ # Test invalid vector shapes
1188
+ with pytest.raises(ValueError, match="Expected vector to have shape"):
1189
+ tf.apply(xp.asarray([1, 2]))
1190
+
1191
+ with pytest.raises(ValueError, match="Expected vector to have shape"):
1192
+ tf.apply(xp.zeros((2, 2)))
1193
+
1194
+ with pytest.raises(ValueError, match="operands could not be broadcast"):
1195
+ tf.apply(xp.zeros((1, 4, 3)))
1196
+
1197
+
1198
+ @make_xp_test_case((RigidTransform, "__getitem__"))
1199
+ def test_indexing_validation(xp):
1200
+ tf = RigidTransform.from_matrix(xp.eye(4))
1201
+
1202
+ # Test indexing on single transform
1203
+ with pytest.raises(TypeError, match="Single transform is not subscriptable"):
1204
+ tf[0]
1205
+
1206
+ with pytest.raises(TypeError, match="Single transform is not subscriptable"):
1207
+ tf[0:1]
1208
+
1209
+ # Test length on single transform
1210
+ with pytest.raises(TypeError, match="Single transform has no len"):
1211
+ len(tf)
1212
+
1213
+
1214
+ @make_xp_test_case((RigidTransform, "__mul__"))
1215
+ def test_composition_validation(xp):
1216
+ tf2 = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6]]))
1217
+ tf3 = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]]))
1218
+
1219
+ # Test incompatible shapes
1220
+ with pytest.raises(ValueError, match="Cannot broadcast"):
1221
+ tf2 * tf3
1222
+
1223
+ tf4 = RigidTransform.from_matrix(xp.tile(xp.eye(4), (1, 4, 1, 1)))
1224
+ # Test invalid broadcasting shape
1225
+ with pytest.raises(ValueError, match="Cannot broadcast"):
1226
+ tf2 * tf4
1227
+
1228
+
1229
+ @make_xp_test_case((RigidTransform, "__mul__"), (RigidTransform, "__rmul__"))
1230
+ def test_rotation_promotion(xp):
1231
+ """Test that Rotation is promoted to RigidTransform in composition."""
1232
+ atol = 1e-12
1233
+ dtype = xpx.default_dtype(xp)
1234
+ rng = np.random.default_rng(0)
1235
+
1236
+ t = xp.asarray(rng.normal(size=(3,)), dtype=dtype)
1237
+ r1 = rotation_to_xp(Rotation.random(rng=rng), xp)
1238
+ r2 = rotation_to_xp(Rotation.random(rng=rng), xp)
1239
+ tf = RigidTransform.from_components(t, r2)
1240
+
1241
+ # RigidTransform * Rotation
1242
+ result = tf * r1
1243
+ expected = tf * RigidTransform.from_rotation(r1)
1244
+ assert isinstance(result, RigidTransform)
1245
+ xp_assert_close(result.as_matrix(), expected.as_matrix(), atol=atol)
1246
+
1247
+ # Rotation * RigidTransform
1248
+ result = r1 * tf
1249
+ expected = RigidTransform.from_rotation(r1) * tf
1250
+ assert isinstance(result, RigidTransform)
1251
+ xp_assert_close(result.as_matrix(), expected.as_matrix(), atol=atol)
1252
+
1253
+
1254
+ @make_xp_test_case((RigidTransform, "concatenate"))
1255
+ def test_concatenate_validation(xp):
1256
+ tf = RigidTransform.from_matrix(xp.eye(4))
1257
+
1258
+ # Test invalid inputs
1259
+ with pytest.raises(TypeError,
1260
+ match="input must contain RigidTransform objects"):
1261
+ RigidTransform.concatenate([tf, xp.eye(4)])
1262
+
1263
+ # Test incompatible shapes
1264
+ tf2 = RigidTransform.from_translation(xp.ones((1, 1, 1, 3)))
1265
+ # Frameworks have a highly heterogeneous way of reporting errors for this case
1266
+ with pytest.raises((ValueError, TypeError, RuntimeError)):
1267
+ RigidTransform.concatenate([tf, tf2])
1268
+
1269
+
1270
+ @make_xp_test_case((RigidTransform, "__setitem__"))
1271
+ def test_setitem(xp):
1272
+ tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]]))
1273
+ single = RigidTransform.from_translation(xp.asarray([1, 1, 1]))
1274
+ double = RigidTransform.from_translation(xp.asarray([[2, 2, 2], [3, 3, 3]]))
1275
+ triple = RigidTransform.from_translation(xp.asarray([[3, 3, 3],
1276
+ [4, 4, 4],
1277
+ [5, 5, 5]]))
1278
+
1279
+ # Test indexing with integer index
1280
+ tf[0] = single
1281
+ xp_assert_close(tf.translation, xp.asarray([[1.0, 1, 1], [4, 5, 6], [7, 8, 9]]))
1282
+
1283
+ # Test indexing with slice
1284
+ tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]]))
1285
+ tf[:2] = double
1286
+ xp_assert_close(tf.translation, xp.asarray([[2.0, 2, 2], [3, 3, 3], [7, 8, 9]]))
1287
+
1288
+ # Test indexing with ellipsis
1289
+ tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]]))
1290
+ tf[...] = triple
1291
+ xp_assert_close(tf.translation, xp.asarray([[3.0, 3, 3], [4, 4, 4], [5, 5, 5]]))
1292
+
1293
+ # Test indexing with boolean array
1294
+ tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]]))
1295
+ mask = xp.asarray([True, False, True])
1296
+ tf[mask] = double
1297
+ xp_assert_close(tf.translation, xp.asarray([[2.0, 2, 2], [4, 5, 6], [3, 3, 3]]))
1298
+
1299
+
1300
+ @make_xp_test_case((RigidTransform, "__setitem__"))
1301
+ @pytest.mark.skip_xp_backends("array_api_strict",
1302
+ reason="doesn't support fancy indexing __setitem__")
1303
+ def test_setitem_fancy_indexing(xp):
1304
+ double = RigidTransform.from_translation(xp.asarray([[2, 2, 2], [3, 3, 3]]))
1305
+ tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]]))
1306
+ idx = xp.asarray([0, 2])
1307
+ tf[idx] = double
1308
+ xp_assert_close(tf.translation, xp.asarray([[2.0, 2, 2], [4, 5, 6], [3, 3, 3]]))
1309
+
1310
+
1311
+ @make_xp_test_case((RigidTransform, "__setitem__"))
1312
+ def test_setitem_validation(xp):
1313
+ tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6]]))
1314
+ single = RigidTransform.from_matrix(xp.eye(4))
1315
+
1316
+ # Test setting item on single transform
1317
+ with pytest.raises(TypeError, match="Single transform is not subscriptable"):
1318
+ single[0] = tf
1319
+
1320
+ # Test invalid value type
1321
+ with pytest.raises(TypeError, match="value must be a RigidTransform"):
1322
+ tf[0] = xp.eye(4)
1323
+
1324
+
1325
+ @pytest.mark.skip_xp_backends("jax.numpy",
1326
+ reason="JAX does not support memory sharing")
1327
+ @make_xp_test_case((RigidTransform, "as_matrix"))
1328
+ def test_copy_flag(xp):
1329
+ # Test that copy=True creates new memory
1330
+ matrix = xp.eye(4)
1331
+ tf = RigidTransform(matrix, normalize=False, copy=True)
1332
+ matrix[0, 0] = 2
1333
+ assert tf.as_matrix()[0, 0] == 1
1334
+
1335
+ # Test that copy=False shares memory
1336
+ matrix = xp.eye(4)
1337
+ tf = RigidTransform(matrix, normalize=False, copy=False)
1338
+ matrix[0, 0] = 2
1339
+ assert tf.as_matrix()[0, 0] == 2
1340
+
1341
+
1342
+ @make_xp_test_case(normalize_dual_quaternion)
1343
+ @pytest.mark.parametrize("ndim", range(1, 4))
1344
+ def test_normalize_dual_quaternion(xp, ndim: int):
1345
+ dtype = xpx.default_dtype(xp)
1346
+ atol = 1e-12 if dtype == xp.float64 else 1e-6
1347
+ rng = np.random.default_rng(100)
1348
+ shape = (ndim,) * (ndim - 1)
1349
+
1350
+ dual_quat = normalize_dual_quaternion(xp.zeros((1, 8)))
1351
+ xp_assert_close(xp_vector_norm(dual_quat[0, :4], axis=-1), xp.asarray(1.0)[()],
1352
+ atol=1e-12)
1353
+ xp_assert_close(xp.vecdot(dual_quat[0, :4], dual_quat[0, 4:])[()],
1354
+ xp.asarray(0.0)[()], atol=1e-12)
1355
+
1356
+ dual_quat = xp.asarray(rng.normal(size=shape + (8,)), dtype=dtype)
1357
+ dual_quat = normalize_dual_quaternion(dual_quat)
1358
+ expected = xp.ones(shape) if shape != () else xp.asarray(1.0)[()]
1359
+ xp_assert_close(xp_vector_norm(dual_quat[..., :4], axis=-1), expected, atol=atol)
1360
+ expected = xp.zeros(shape) if shape != () else xp.asarray(0.0)[()]
1361
+ vecdot = xp.vecdot(dual_quat[..., :4], dual_quat[..., 4:])
1362
+ vecdot = vecdot[()] if vecdot.shape == () else vecdot
1363
+ xp_assert_close(vecdot, expected, atol=atol)
1364
+
1365
+
1366
+ @make_xp_test_case(
1367
+ (RigidTransform, "from_matrix"), (RigidTransform, "from_rotation"),
1368
+ (RigidTransform, "from_translation"), (RigidTransform, "from_components"),
1369
+ (RigidTransform, "from_exp_coords"), (RigidTransform, "from_dual_quat")
1370
+ )
1371
+ def test_empty_transform_construction(xp):
1372
+ tf = RigidTransform.from_matrix(xp.empty((0, 4, 4)))
1373
+ assert len(tf) == 0
1374
+ assert not tf.single
1375
+
1376
+ tf = RigidTransform.from_rotation(Rotation.from_quat(xp.zeros((0, 4))))
1377
+ assert len(tf) == 0
1378
+ assert not tf.single
1379
+
1380
+ tf = RigidTransform.from_translation(xp.empty((0, 3)))
1381
+ assert len(tf) == 0
1382
+ assert not tf.single
1383
+
1384
+ empty_rot = Rotation.from_quat(xp.zeros((0, 4)))
1385
+ tf = RigidTransform.from_components(xp.empty((0, 3)), empty_rot)
1386
+ assert len(tf) == 0
1387
+ assert not tf.single
1388
+
1389
+ tf = RigidTransform.from_exp_coords(xp.empty((0, 6)))
1390
+ assert len(tf) == 0
1391
+ assert not tf.single
1392
+
1393
+ tf = RigidTransform.from_dual_quat(xp.empty((0, 8)))
1394
+ assert len(tf) == 0
1395
+ assert not tf.single
1396
+
1397
+ tf = RigidTransform.identity(0)
1398
+ assert len(tf) == 0
1399
+ assert not tf.single
1400
+
1401
+
1402
+ @make_xp_test_case((RigidTransform, "from_matrix"), (RigidTransform, "as_components"),
1403
+ (RigidTransform, "as_exp_coords"), (RigidTransform, "as_dual_quat"))
1404
+ def test_empty_transform_representation(xp):
1405
+ tf = RigidTransform.from_matrix(xp.empty((0, 4, 4)))
1406
+
1407
+ assert len(tf.rotation) == 0
1408
+ assert tf.translation.shape == (0, 3)
1409
+
1410
+ t, r = tf.as_components()
1411
+ assert t.shape == (0, 3)
1412
+ assert len(r) == 0
1413
+
1414
+ assert tf.as_matrix().shape == (0, 4, 4)
1415
+ assert tf.as_exp_coords().shape == (0, 6)
1416
+ assert tf.as_dual_quat().shape == (0, 8)
1417
+
1418
+
1419
+ @make_xp_test_case((RigidTransform, "from_matrix"), (RigidTransform, "apply"))
1420
+ def test_empty_transform_application(xp):
1421
+ tf = RigidTransform.from_matrix(xp.empty((0, 4, 4)))
1422
+
1423
+ assert tf.apply(xp.zeros((3,))).shape == (0, 3)
1424
+ assert tf.apply(xp.empty((0, 3))).shape == (0, 3)
1425
+
1426
+ with pytest.raises(ValueError, match="operands could not be broadcast together"):
1427
+ tf.apply(xp.zeros((2, 3)))
1428
+
1429
+
1430
+ @make_xp_test_case((RigidTransform, "from_matrix"), (RigidTransform, "__mul__"))
1431
+ def test_empty_transform_composition(xp):
1432
+ tf_empty = RigidTransform.from_matrix(xp.empty((0, 4, 4)))
1433
+ tf_single = RigidTransform.from_matrix(xp.eye(4))
1434
+ tf_many = rigid_transform_to_xp(RigidTransform.identity(3), xp=xp)
1435
+
1436
+ assert len(tf_empty * tf_empty) == 0
1437
+ assert len(tf_empty * tf_single) == 0
1438
+ assert len(tf_single * tf_empty) == 0
1439
+
1440
+ with pytest.raises(ValueError, match="Cannot broadcast"):
1441
+ tf_many * tf_empty
1442
+
1443
+ with pytest.raises(ValueError, match="Cannot broadcast"):
1444
+ tf_empty * tf_many
1445
+
1446
+
1447
+ @make_xp_test_case((RigidTransform, "from_matrix"), (RigidTransform, "concatenate"))
1448
+ def test_empty_transform_concatenation(xp):
1449
+ tf_empty = RigidTransform.from_matrix(xp.empty((0, 4, 4)))
1450
+ tf_single = RigidTransform.from_matrix(xp.eye(4))
1451
+ tf_many = rigid_transform_to_xp(RigidTransform.identity(2), xp=xp)
1452
+
1453
+ assert len(RigidTransform.concatenate([tf_empty, tf_empty])) == 0
1454
+ assert len(RigidTransform.concatenate([tf_empty, tf_single])) == 1
1455
+ assert len(RigidTransform.concatenate([tf_single, tf_empty])) == 1
1456
+ assert len(RigidTransform.concatenate([tf_empty, tf_many])) == 2
1457
+ assert len(RigidTransform.concatenate([tf_many, tf_empty])) == 2
1458
+ assert len(RigidTransform.concatenate([tf_many, tf_empty, tf_single])) == 3
1459
+
1460
+
1461
+ @make_xp_test_case((RigidTransform, "from_matrix"), (RigidTransform, "inv"),
1462
+ (RigidTransform, "__pow__"))
1463
+ def test_empty_transform_inv_and_pow(xp):
1464
+ tf = RigidTransform.from_matrix(xp.empty((0, 4, 4)))
1465
+ assert len(tf.inv()) == 0
1466
+ assert len(tf ** 0) == 0
1467
+ assert len(tf ** 1) == 0
1468
+ assert len(tf ** -1) == 0
1469
+ assert len(tf ** 0.5) == 0
1470
+
1471
+
1472
+ @make_xp_test_case((RigidTransform, "__getitem__"))
1473
+ def test_empty_transform_indexing(xp):
1474
+ tf_many = rigid_transform_to_xp(RigidTransform.identity(3), xp=xp)
1475
+ tf_zero = tf_many[xp.asarray([], dtype=xp.int32)]
1476
+ assert len(tf_zero) == 0
1477
+
1478
+ # Array API does not specify out-of-bounds indexing. Only check for numpy.
1479
+ if is_numpy(xp):
1480
+ assert len(tf_zero[:5]) == 0 # Slices can go out of bounds.
1481
+
1482
+ with pytest.raises(IndexError):
1483
+ tf_zero[0]
1484
+
1485
+ with pytest.raises(IndexError):
1486
+ tf_zero[xp.asarray([0, 2])]
1487
+
1488
+ with pytest.raises(IndexError):
1489
+ tf_zero[xp.asarray([False, True])]
1490
+
1491
+
1492
+ @make_xp_test_case((RigidTransform, "from_matrix"))
1493
+ @pytest.mark.skip_xp_backends("array_api_strict",
1494
+ reason="array API doesn't support pickling")
1495
+ def test_pickling(xp):
1496
+ # Note: Array API makes no provision for arrays to be pickleable, so
1497
+ # it's OK to skip this test for the backends that don't support it
1498
+ mat = xp.eye(4)
1499
+ mat = xpx.at(mat)[0, 3].set(2.0)
1500
+ tf = RigidTransform.from_matrix(mat)
1501
+ pkl = pickle.dumps(tf)
1502
+ unpickled = pickle.loads(pkl)
1503
+ xp_assert_close(tf.as_matrix(), unpickled.as_matrix(), atol=1e-15)
1504
+
1505
+
1506
+ @make_xp_test_case(
1507
+ (RigidTransform, "as_matrix"),
1508
+ (RigidTransform, "__iter__"),
1509
+ (RigidTransform, "identity"),
1510
+ )
1511
+ def test_rigid_transform_iter(xp):
1512
+ r = rigid_transform_to_xp(RigidTransform.identity(3), xp)
1513
+ for i, r_i in enumerate(r):
1514
+ assert isinstance(r_i, RigidTransform)
1515
+ xp_assert_equal(r_i.as_matrix(), r[i].as_matrix())
1516
+ if i > len(r):
1517
+ raise RuntimeError("Iteration exceeded length of transforms")
1518
+
1519
+
1520
+ @make_xp_test_case((RigidTransform, "from_translation"))
1521
+ @pytest.mark.parametrize("dim", range(1, 5))
1522
+ def test_shape_property(xp, dim: int):
1523
+ shape = (dim,) * (dim - 1)
1524
+ tf = RigidTransform.from_translation(xp.zeros(shape + (3,)))
1525
+ assert tf.shape == shape
1526
+
1527
+
1528
+ def test_non_writeable():
1529
+ mat = np.eye(4)
1530
+ mat.flags.writeable = False
1531
+ RigidTransform.from_matrix(mat) # Regression test against gh-24378
v12x/pylib/scipy/spatial/transform/tests/test_rotation.py ADDED
The diff for this file is too large to render. See raw diff
 
v12x/pylib/scipy/spatial/transform/tests/test_rotation_groups.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ import numpy as np
4
+ from numpy.testing import assert_array_almost_equal
5
+ from scipy.spatial.transform import Rotation
6
+ from scipy.optimize import linear_sum_assignment
7
+ from scipy.spatial.distance import cdist
8
+ from scipy.constants import golden as phi
9
+ from scipy.spatial import cKDTree
10
+
11
+
12
+ TOL = 1E-12
13
+ NS = list(range(1, 13))
14
+ NAMES = ["I", "O", "T"] + [f"C{n}" for n in NS] + [f"D{n}" for n in NS]
15
+ SIZES = [60, 24, 12] + list(NS) + [2 * n for n in NS]
16
+
17
+
18
+ def _calculate_rmsd(P, Q):
19
+ """Calculates the root-mean-square distance between the points of P and Q.
20
+ The distance is taken as the minimum over all possible matchings. It is
21
+ zero if P and Q are identical and non-zero if not.
22
+ """
23
+ distance_matrix = cdist(P, Q, metric='sqeuclidean')
24
+ matching = linear_sum_assignment(distance_matrix)
25
+ return np.sqrt(distance_matrix[matching].sum())
26
+
27
+
28
+ def _generate_pyramid(n, axis):
29
+ thetas = np.linspace(0, 2 * np.pi, n + 1)[:-1]
30
+ P = np.vstack([np.zeros(n), np.cos(thetas), np.sin(thetas)]).T
31
+ P = np.concatenate((P, [[1, 0, 0]]))
32
+ return np.roll(P, axis, axis=1)
33
+
34
+
35
+ def _generate_prism(n, axis):
36
+ thetas = np.linspace(0, 2 * np.pi, n + 1)[:-1]
37
+ bottom = np.vstack([-np.ones(n), np.cos(thetas), np.sin(thetas)]).T
38
+ top = np.vstack([+np.ones(n), np.cos(thetas), np.sin(thetas)]).T
39
+ P = np.concatenate((bottom, top))
40
+ return np.roll(P, axis, axis=1)
41
+
42
+
43
+ def _generate_icosahedron():
44
+ x = np.array([[0, -1, -phi],
45
+ [0, -1, +phi],
46
+ [0, +1, -phi],
47
+ [0, +1, +phi]])
48
+ return np.concatenate([np.roll(x, i, axis=1) for i in range(3)])
49
+
50
+
51
+ def _generate_octahedron():
52
+ return np.array([[-1, 0, 0], [+1, 0, 0], [0, -1, 0],
53
+ [0, +1, 0], [0, 0, -1], [0, 0, +1]])
54
+
55
+
56
+ def _generate_tetrahedron():
57
+ return np.array([[1, 1, 1], [1, -1, -1], [-1, 1, -1], [-1, -1, 1]])
58
+
59
+
60
+ @pytest.mark.parametrize("name", [-1, None, True, np.array(['C3'])])
61
+ def test_group_type(name):
62
+ with pytest.raises(ValueError,
63
+ match="must be a string"):
64
+ Rotation.create_group(name)
65
+
66
+
67
+ @pytest.mark.parametrize("name", ["Q", " ", "CA", "C ", "DA", "D ", "I2", ""])
68
+ def test_group_name(name):
69
+ with pytest.raises(ValueError,
70
+ match="must be one of 'I', 'O', 'T', 'Dn', 'Cn'"):
71
+ Rotation.create_group(name)
72
+
73
+
74
+ @pytest.mark.parametrize("name", ["C0", "D0"])
75
+ def test_group_order_positive(name):
76
+ with pytest.raises(ValueError,
77
+ match="Group order must be positive"):
78
+ Rotation.create_group(name)
79
+
80
+
81
+ @pytest.mark.parametrize("axis", ['A', 'b', 0, 1, 2, 4, False, None])
82
+ def test_axis_valid(axis):
83
+ with pytest.raises(ValueError,
84
+ match="`axis` must be one of"):
85
+ Rotation.create_group("C1", axis)
86
+
87
+
88
+ def test_icosahedral():
89
+ """The icosahedral group fixes the rotations of an icosahedron. Here we
90
+ test that the icosahedron is invariant after application of the elements
91
+ of the rotation group."""
92
+ P = _generate_icosahedron()
93
+ for g in Rotation.create_group("I"):
94
+ g = Rotation.from_quat(g.as_quat())
95
+ assert _calculate_rmsd(P, g.apply(P)) < TOL
96
+
97
+
98
+ def test_octahedral():
99
+ """Test that the octahedral group correctly fixes the rotations of an
100
+ octahedron."""
101
+ P = _generate_octahedron()
102
+ for g in Rotation.create_group("O"):
103
+ assert _calculate_rmsd(P, g.apply(P)) < TOL
104
+
105
+
106
+ def test_tetrahedral():
107
+ """Test that the tetrahedral group correctly fixes the rotations of a
108
+ tetrahedron."""
109
+ P = _generate_tetrahedron()
110
+ for g in Rotation.create_group("T"):
111
+ assert _calculate_rmsd(P, g.apply(P)) < TOL
112
+
113
+
114
+ @pytest.mark.parametrize("n", NS)
115
+ @pytest.mark.parametrize("axis", 'XYZ')
116
+ def test_dicyclic(n, axis):
117
+ """Test that the dicyclic group correctly fixes the rotations of a
118
+ prism."""
119
+ P = _generate_prism(n, axis='XYZ'.index(axis))
120
+ for g in Rotation.create_group(f"D{n}", axis=axis):
121
+ assert _calculate_rmsd(P, g.apply(P)) < TOL
122
+
123
+
124
+ @pytest.mark.parametrize("n", NS)
125
+ @pytest.mark.parametrize("axis", 'XYZ')
126
+ def test_cyclic(n, axis):
127
+ """Test that the cyclic group correctly fixes the rotations of a
128
+ pyramid."""
129
+ P = _generate_pyramid(n, axis='XYZ'.index(axis))
130
+ for g in Rotation.create_group(f"C{n}", axis=axis):
131
+ assert _calculate_rmsd(P, g.apply(P)) < TOL
132
+
133
+
134
+ @pytest.mark.parametrize("name, size", list(zip(NAMES, SIZES)))
135
+ def test_group_sizes(name, size):
136
+ assert len(Rotation.create_group(name)) == size
137
+
138
+
139
+ @pytest.mark.parametrize("name, size", list(zip(NAMES, SIZES)))
140
+ def test_group_no_duplicates(name, size):
141
+ g = Rotation.create_group(name)
142
+ kdtree = cKDTree(g.as_quat())
143
+ assert len(kdtree.query_pairs(1E-3)) == 0
144
+
145
+
146
+ @pytest.mark.parametrize("name, size", list(zip(NAMES, SIZES)))
147
+ def test_group_symmetry(name, size):
148
+ g = Rotation.create_group(name)
149
+ q = np.concatenate((-g.as_quat(), g.as_quat()))
150
+ distance = np.sort(cdist(q, q))
151
+ deltas = np.max(distance, axis=0) - np.min(distance, axis=0)
152
+ assert (deltas < TOL).all()
153
+
154
+
155
+ @pytest.mark.parametrize("name", NAMES)
156
+ def test_reduction(name):
157
+ """Test that the elements of the rotation group are correctly
158
+ mapped onto the identity rotation."""
159
+ g = Rotation.create_group(name)
160
+ f = g.reduce(g)
161
+ assert_array_almost_equal(f.magnitude(), np.zeros(len(g)))
162
+
163
+
164
+ @pytest.mark.parametrize("name", NAMES)
165
+ def test_single_reduction(name):
166
+ g = Rotation.create_group(name)
167
+ f = g[-1].reduce(g)
168
+ assert_array_almost_equal(f.magnitude(), 0)
169
+ assert f.as_quat().shape == (4,)
v12x/pylib/scipy/spatial/transform/tests/test_rotation_spline.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from itertools import product
2
+ import numpy as np
3
+ from numpy.testing import assert_allclose
4
+ import pytest
5
+ from scipy.spatial.transform import Rotation, RotationSpline
6
+ from scipy.spatial.transform._rotation_spline import (
7
+ _angular_rate_to_rotvec_dot_matrix,
8
+ _rotvec_dot_to_angular_rate_matrix,
9
+ _matrix_vector_product_of_stacks,
10
+ _angular_acceleration_nonlinear_term,
11
+ _create_block_3_diagonal_matrix)
12
+
13
+
14
+ pytestmark = pytest.mark.skip_xp_backends(np_only=True)
15
+
16
+
17
+ def test_angular_rate_to_rotvec_conversions():
18
+ np.random.seed(0)
19
+ rv = np.random.randn(4, 3)
20
+ A = _angular_rate_to_rotvec_dot_matrix(rv)
21
+ A_inv = _rotvec_dot_to_angular_rate_matrix(rv)
22
+
23
+ # When the rotation vector is aligned with the angular rate, then
24
+ # the rotation vector rate and angular rate are the same.
25
+ assert_allclose(_matrix_vector_product_of_stacks(A, rv), rv)
26
+ assert_allclose(_matrix_vector_product_of_stacks(A_inv, rv), rv)
27
+
28
+ # A and A_inv must be reciprocal to each other.
29
+ I_stack = np.empty((4, 3, 3))
30
+ I_stack[:] = np.eye(3)
31
+ assert_allclose(np.matmul(A, A_inv), I_stack, atol=1e-15)
32
+
33
+
34
+ def test_angular_rate_nonlinear_term():
35
+ # The only simple test is to check that the term is zero when
36
+ # the rotation vector
37
+ np.random.seed(0)
38
+ rv = np.random.rand(4, 3)
39
+ assert_allclose(_angular_acceleration_nonlinear_term(rv, rv), 0,
40
+ atol=1e-19)
41
+
42
+
43
+ def test_create_block_3_diagonal_matrix():
44
+ np.random.seed(0)
45
+ A = np.empty((4, 3, 3))
46
+ A[:] = np.arange(1, 5)[:, None, None]
47
+
48
+ B = np.empty((4, 3, 3))
49
+ B[:] = -np.arange(1, 5)[:, None, None]
50
+ d = 10 * np.arange(10, 15)
51
+
52
+ banded = _create_block_3_diagonal_matrix(A, B, d)
53
+
54
+ # Convert the banded matrix to the full matrix.
55
+ k, l = list(zip(*product(np.arange(banded.shape[0]),
56
+ np.arange(banded.shape[1]))))
57
+ k = np.asarray(k)
58
+ l = np.asarray(l)
59
+
60
+ i = k - 5 + l
61
+ j = l
62
+ values = banded.ravel()
63
+ mask = (i >= 0) & (i < 15)
64
+ i = i[mask]
65
+ j = j[mask]
66
+ values = values[mask]
67
+ full = np.zeros((15, 15))
68
+ full[i, j] = values
69
+
70
+ zero = np.zeros((3, 3))
71
+ eye = np.eye(3)
72
+
73
+ # Create the reference full matrix in the most straightforward manner.
74
+ ref = np.block([
75
+ [d[0] * eye, B[0], zero, zero, zero],
76
+ [A[0], d[1] * eye, B[1], zero, zero],
77
+ [zero, A[1], d[2] * eye, B[2], zero],
78
+ [zero, zero, A[2], d[3] * eye, B[3]],
79
+ [zero, zero, zero, A[3], d[4] * eye],
80
+ ])
81
+
82
+ assert_allclose(full, ref, atol=1e-19)
83
+
84
+
85
+ def test_spline_2_rotations():
86
+ times = [0, 10]
87
+ rotations = Rotation.from_euler('xyz', [[0, 0, 0], [10, -20, 30]],
88
+ degrees=True)
89
+ spline = RotationSpline(times, rotations)
90
+
91
+ rv = (rotations[0].inv() * rotations[1]).as_rotvec()
92
+ rate = rv / (times[1] - times[0])
93
+ times_check = np.array([-1, 5, 12])
94
+ dt = times_check - times[0]
95
+ rv_ref = rate * dt[:, None]
96
+
97
+ assert_allclose(spline(times_check).as_rotvec(), rv_ref)
98
+ assert_allclose(spline(times_check, 1), np.resize(rate, (3, 3)))
99
+ assert_allclose(spline(times_check, 2), 0, atol=1e-16)
100
+
101
+
102
+ def test_constant_attitude():
103
+ times = np.arange(10)
104
+ rotations = Rotation.from_rotvec(np.ones((10, 3)))
105
+ spline = RotationSpline(times, rotations)
106
+
107
+ times_check = np.linspace(-1, 11)
108
+ assert_allclose(spline(times_check).as_rotvec(), 1, rtol=1e-15)
109
+ assert_allclose(spline(times_check, 1), 0, atol=1e-17)
110
+ assert_allclose(spline(times_check, 2), 0, atol=1e-17)
111
+
112
+ assert_allclose(spline(5.5).as_rotvec(), 1, rtol=1e-15)
113
+ assert_allclose(spline(5.5, 1), 0, atol=1e-17)
114
+ assert_allclose(spline(5.5, 2), 0, atol=1e-17)
115
+
116
+
117
+ def test_spline_properties():
118
+ times = np.array([0, 5, 15, 27])
119
+ angles = [[-5, 10, 27], [3, 5, 38], [-12, 10, 25], [-15, 20, 11]]
120
+
121
+ rotations = Rotation.from_euler('xyz', angles, degrees=True)
122
+ spline = RotationSpline(times, rotations)
123
+
124
+ assert_allclose(spline(times).as_euler('xyz', degrees=True), angles)
125
+ assert_allclose(spline(0).as_euler('xyz', degrees=True), angles[0])
126
+
127
+ h = 1e-8
128
+ rv0 = spline(times).as_rotvec()
129
+ rvm = spline(times - h).as_rotvec()
130
+ rvp = spline(times + h).as_rotvec()
131
+ # rtol bumped from 1e-15 to 1.5e-15 in gh18414 for linux 32 bit
132
+ assert_allclose(rv0, 0.5 * (rvp + rvm), rtol=1.5e-15)
133
+
134
+ r0 = spline(times, 1)
135
+ rm = spline(times - h, 1)
136
+ rp = spline(times + h, 1)
137
+ assert_allclose(r0, 0.5 * (rm + rp), rtol=1e-14)
138
+
139
+ a0 = spline(times, 2)
140
+ am = spline(times - h, 2)
141
+ ap = spline(times + h, 2)
142
+ assert_allclose(a0, am, rtol=1e-7)
143
+ assert_allclose(a0, ap, rtol=1e-7)
144
+
145
+
146
+ def test_error_handling():
147
+ with pytest.raises(ValueError):
148
+ RotationSpline([1.0], Rotation.random())
149
+
150
+ r = Rotation.random(10)
151
+ t = np.arange(10).reshape(5, 2)
152
+ with pytest.raises(ValueError):
153
+ RotationSpline(t, r)
154
+
155
+ t = np.arange(9)
156
+ with pytest.raises(ValueError):
157
+ RotationSpline(t, r)
158
+
159
+ t = np.arange(10)
160
+ t[5] = 0
161
+ with pytest.raises(ValueError):
162
+ RotationSpline(t, r)
163
+
164
+ t = np.arange(10)
165
+
166
+ s = RotationSpline(t, r)
167
+ with pytest.raises(ValueError):
168
+ s(10, -1)
169
+
170
+ with pytest.raises(ValueError):
171
+ s(np.arange(10).reshape(5, 2))
172
+
173
+ r = Rotation.from_quat(np.array([[[0.0, 0, 0, 1], [1, 0, 0 ,0]]]))
174
+ t = np.arange(2)
175
+ with pytest.raises(ValueError):
176
+ RotationSpline(t, r) # More than 2 dimensions
177
+
178
+
179
+ @pytest.mark.skip_xp_backends("numpy")
180
+ def test_xp_errors(xp):
181
+ times = xp.asarray([0, 10])
182
+ r = Rotation.random(2)
183
+ r = Rotation.from_quat(xp.asarray(r.as_quat()))
184
+ s = RotationSpline(times, r)
185
+ t = xp.asarray([0.5, 1.5])
186
+ # RotationSpline does not have native Array API support, so we check that it
187
+ # converts any array to NumPy and outputs NumPy arrays.
188
+ assert isinstance(s(t).as_quat(), np.ndarray)
v12x/pylib/scipy/special/__init__.py ADDED
@@ -0,0 +1,842 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ========================================
3
+ Special functions (:mod:`scipy.special`)
4
+ ========================================
5
+
6
+ .. currentmodule:: scipy.special
7
+
8
+ .. toctree::
9
+ :hidden:
10
+
11
+ special.cython_special
12
+
13
+ Almost all of the functions below accept NumPy arrays as input
14
+ arguments as well as single numbers. This means they follow
15
+ broadcasting and automatic array-looping rules. Technically,
16
+ they are `NumPy universal functions
17
+ <https://numpy.org/doc/stable/user/basics.ufuncs.html#ufuncs-basics>`_.
18
+ Functions which do not accept NumPy arrays are marked by a warning
19
+ in the section description.
20
+
21
+ .. seealso::
22
+
23
+ `scipy.special.cython_special` -- Typed Cython versions of special functions
24
+
25
+
26
+ Error handling
27
+ ==============
28
+
29
+ Errors are handled by returning NaNs or other appropriate values.
30
+ Some of the special function routines can emit warnings or raise
31
+ exceptions when an error occurs. By default this is disabled, except
32
+ for memory allocation errors, which result in an exception being raised.
33
+ To query and control the current error handling state the following
34
+ functions are provided.
35
+
36
+ .. autosummary::
37
+ :toctree: generated/
38
+
39
+ geterr -- Get the current way of handling special-function errors.
40
+ seterr -- Set how special-function errors are handled.
41
+ errstate -- Context manager for special-function error handling.
42
+ SpecialFunctionWarning -- Warning that can be emitted by special functions.
43
+ SpecialFunctionError -- Exception that can be raised by special functions.
44
+
45
+ Available functions
46
+ ===================
47
+
48
+ Airy functions
49
+ --------------
50
+
51
+ .. autosummary::
52
+ :toctree: generated/
53
+
54
+ airy -- Airy functions and their derivatives.
55
+ airye -- Exponentially scaled Airy functions and their derivatives.
56
+ ai_zeros -- Compute `nt` zeros and values of the Airy function Ai and its derivative.
57
+ bi_zeros -- Compute `nt` zeros and values of the Airy function Bi and its derivative.
58
+ itairy -- Integrals of Airy functions
59
+
60
+
61
+ Elliptic functions and integrals
62
+ --------------------------------
63
+
64
+ .. autosummary::
65
+ :toctree: generated/
66
+
67
+ ellipj -- Jacobian elliptic functions.
68
+ ellipk -- Complete elliptic integral of the first kind.
69
+ ellipkm1 -- Complete elliptic integral of the first kind around `m` = 1.
70
+ ellipkinc -- Incomplete elliptic integral of the first kind.
71
+ ellipe -- Complete elliptic integral of the second kind.
72
+ ellipeinc -- Incomplete elliptic integral of the second kind.
73
+ elliprc -- Degenerate symmetric integral RC.
74
+ elliprd -- Symmetric elliptic integral of the second kind.
75
+ elliprf -- Completely-symmetric elliptic integral of the first kind.
76
+ elliprg -- Completely-symmetric elliptic integral of the second kind.
77
+ elliprj -- Symmetric elliptic integral of the third kind.
78
+
79
+ Bessel functions
80
+ ----------------
81
+
82
+ .. autosummary::
83
+ :toctree: generated/
84
+
85
+ jv -- Bessel function of the first kind of real order and \
86
+ complex argument.
87
+ jve -- Exponentially scaled Bessel function of order `v`.
88
+ yn -- Bessel function of the second kind of integer order and \
89
+ real argument.
90
+ yv -- Bessel function of the second kind of real order and \
91
+ complex argument.
92
+ yve -- Exponentially scaled Bessel function of the second kind \
93
+ of real order.
94
+ iv -- Modified Bessel function of the first kind of real order.
95
+ ive -- Exponentially scaled modified Bessel function of the \
96
+ first kind.
97
+ kn -- Modified Bessel function of the second kind of integer \
98
+ order `n`
99
+ kv -- Modified Bessel function of the second kind of real order \
100
+ `v`
101
+ kve -- Exponentially scaled modified Bessel function of the \
102
+ second kind.
103
+ hankel1 -- Hankel function of the first kind.
104
+ hankel1e -- Exponentially scaled Hankel function of the first kind.
105
+ hankel2 -- Hankel function of the second kind.
106
+ hankel2e -- Exponentially scaled Hankel function of the second kind.
107
+ wright_bessel -- Wright's generalized Bessel function.
108
+ log_wright_bessel -- Logarithm of Wright's generalized Bessel function.
109
+
110
+ The following function does not accept NumPy arrays (it is not a
111
+ universal function):
112
+
113
+ .. autosummary::
114
+ :toctree: generated/
115
+
116
+ lmbda -- Jahnke-Emden Lambda function, Lambdav(x).
117
+
118
+ Zeros of Bessel functions
119
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
120
+
121
+ The following functions do not accept NumPy arrays (they are not
122
+ universal functions):
123
+
124
+ .. autosummary::
125
+ :toctree: generated/
126
+
127
+ jnjnp_zeros -- Compute zeros of integer-order Bessel functions Jn and Jn'.
128
+ jnyn_zeros -- Compute nt zeros of Bessel functions Jn(x), Jn'(x), Yn(x), and Yn'(x).
129
+ jn_zeros -- Compute zeros of integer-order Bessel function Jn(x).
130
+ jnp_zeros -- Compute zeros of integer-order Bessel function derivative Jn'(x).
131
+ yn_zeros -- Compute zeros of integer-order Bessel function Yn(x).
132
+ ynp_zeros -- Compute zeros of integer-order Bessel function derivative Yn'(x).
133
+ y0_zeros -- Compute nt zeros of Bessel function Y0(z), and derivative at each zero.
134
+ y1_zeros -- Compute nt zeros of Bessel function Y1(z), and derivative at each zero.
135
+ y1p_zeros -- Compute nt zeros of Bessel derivative Y1'(z), and value at each zero.
136
+
137
+ Faster versions of common Bessel functions
138
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
139
+
140
+ .. autosummary::
141
+ :toctree: generated/
142
+
143
+ j0 -- Bessel function of the first kind of order 0.
144
+ j1 -- Bessel function of the first kind of order 1.
145
+ y0 -- Bessel function of the second kind of order 0.
146
+ y1 -- Bessel function of the second kind of order 1.
147
+ i0 -- Modified Bessel function of order 0.
148
+ i0e -- Exponentially scaled modified Bessel function of order 0.
149
+ i1 -- Modified Bessel function of order 1.
150
+ i1e -- Exponentially scaled modified Bessel function of order 1.
151
+ k0 -- Modified Bessel function of the second kind of order 0, :math:`K_0`.
152
+ k0e -- Exponentially scaled modified Bessel function K of order 0
153
+ k1 -- Modified Bessel function of the second kind of order 1, :math:`K_1(x)`.
154
+ k1e -- Exponentially scaled modified Bessel function K of order 1.
155
+
156
+ Integrals of Bessel functions
157
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
158
+
159
+ .. autosummary::
160
+ :toctree: generated/
161
+
162
+ itj0y0 -- Integrals of Bessel functions of order 0.
163
+ it2j0y0 -- Integrals related to Bessel functions of order 0.
164
+ iti0k0 -- Integrals of modified Bessel functions of order 0.
165
+ it2i0k0 -- Integrals related to modified Bessel functions of order 0.
166
+ besselpoly -- Weighted integral of a Bessel function.
167
+
168
+ Derivatives of Bessel functions
169
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
170
+
171
+ .. autosummary::
172
+ :toctree: generated/
173
+
174
+ jvp -- Compute nth derivative of Bessel function Jv(z) with respect to `z`.
175
+ yvp -- Compute nth derivative of Bessel function Yv(z) with respect to `z`.
176
+ ivp -- Compute nth derivative of modified Bessel function Iv(z) with respect to `z`.
177
+ kvp -- Compute nth derivative of real-order modified Bessel function Kv(z)
178
+ h1vp -- Compute nth derivative of Hankel function H1v(z) with respect to `z`.
179
+ h2vp -- Compute nth derivative of Hankel function H2v(z) with respect to `z`.
180
+
181
+ Spherical Bessel functions
182
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
183
+
184
+ .. autosummary::
185
+ :toctree: generated/
186
+
187
+ spherical_jn -- Spherical Bessel function of the first kind or its derivative.
188
+ spherical_yn -- Spherical Bessel function of the second kind or its derivative.
189
+ spherical_in -- Modified spherical Bessel function of the first kind or its derivative.
190
+ spherical_kn -- Modified spherical Bessel function of the second kind or its derivative.
191
+
192
+ Riccati-Bessel functions
193
+ ^^^^^^^^^^^^^^^^^^^^^^^^
194
+
195
+ The following functions do not accept NumPy arrays (they are not
196
+ universal functions):
197
+
198
+ .. autosummary::
199
+ :toctree: generated/
200
+
201
+ riccati_jn -- Compute Riccati-Bessel function of the first kind and its derivative.
202
+ riccati_yn -- Compute Riccati-Bessel function of the second kind and its derivative.
203
+
204
+ Struve functions
205
+ ----------------
206
+
207
+ .. autosummary::
208
+ :toctree: generated/
209
+
210
+ struve -- Struve function.
211
+ modstruve -- Modified Struve function.
212
+ itstruve0 -- Integral of the Struve function of order 0.
213
+ it2struve0 -- Integral related to the Struve function of order 0.
214
+ itmodstruve0 -- Integral of the modified Struve function of order 0.
215
+
216
+
217
+ Raw statistical functions
218
+ -------------------------
219
+
220
+ .. seealso:: :mod:`scipy.stats`: Friendly versions of these functions.
221
+
222
+ Binomial distribution
223
+ ^^^^^^^^^^^^^^^^^^^^^
224
+
225
+ .. autosummary::
226
+ :toctree: generated/
227
+
228
+ bdtr -- Binomial distribution cumulative distribution function.
229
+ bdtrc -- Binomial distribution survival function.
230
+ bdtri -- Inverse function to `bdtr` with respect to `p`.
231
+ bdtrik -- Inverse function to `bdtr` with respect to `k`.
232
+ bdtrin -- Inverse function to `bdtr` with respect to `n`.
233
+
234
+ Beta distribution
235
+ ^^^^^^^^^^^^^^^^^
236
+
237
+ .. autosummary::
238
+ :toctree: generated/
239
+
240
+ btdtria -- Inverse of `betainc` with respect to `a`.
241
+ btdtrib -- Inverse of `betainc` with respect to `b`.
242
+
243
+ F distribution
244
+ ^^^^^^^^^^^^^^
245
+
246
+ .. autosummary::
247
+ :toctree: generated/
248
+
249
+ fdtr -- F cumulative distribution function.
250
+ fdtrc -- F survival function.
251
+ fdtri -- The `p`-th quantile of the F-distribution.
252
+ fdtridfd -- Inverse to `fdtr` vs dfd.
253
+
254
+ Gamma distribution
255
+ ^^^^^^^^^^^^^^^^^^
256
+
257
+ .. autosummary::
258
+ :toctree: generated/
259
+
260
+ gdtr -- Gamma distribution cumulative distribution function.
261
+ gdtrc -- Gamma distribution survival function.
262
+ gdtria -- Inverse of `gdtr` vs a.
263
+ gdtrib -- Inverse of `gdtr` vs b.
264
+ gdtrix -- Inverse of `gdtr` vs x.
265
+
266
+ Negative binomial distribution
267
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
268
+
269
+ .. autosummary::
270
+ :toctree: generated/
271
+
272
+ nbdtr -- Negative binomial cumulative distribution function.
273
+ nbdtrc -- Negative binomial survival function.
274
+ nbdtri -- Inverse of `nbdtr` vs `p`.
275
+ nbdtrik -- Inverse of `nbdtr` vs `k`.
276
+ nbdtrin -- Inverse of `nbdtr` vs `n`.
277
+
278
+ Noncentral F distribution
279
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
280
+
281
+ .. autosummary::
282
+ :toctree: generated/
283
+
284
+ ncfdtr -- Cumulative distribution function of the non-central F distribution.
285
+ ncfdtridfd -- Calculate degrees of freedom (denominator) for the noncentral F-distribution.
286
+ ncfdtridfn -- Calculate degrees of freedom (numerator) for the noncentral F-distribution.
287
+ ncfdtri -- Inverse cumulative distribution function of the non-central F distribution.
288
+ ncfdtrinc -- Calculate non-centrality parameter for non-central F distribution.
289
+
290
+ Noncentral t distribution
291
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
292
+
293
+ .. autosummary::
294
+ :toctree: generated/
295
+
296
+ nctdtr -- Cumulative distribution function of the non-central `t` distribution.
297
+ nctdtridf -- Calculate degrees of freedom for non-central t distribution.
298
+ nctdtrit -- Inverse cumulative distribution function of the non-central t distribution.
299
+ nctdtrinc -- Calculate non-centrality parameter for non-central t distribution.
300
+
301
+ Normal distribution
302
+ ^^^^^^^^^^^^^^^^^^^
303
+
304
+ .. autosummary::
305
+ :toctree: generated/
306
+
307
+ nrdtrimn -- Calculate mean of normal distribution given other params.
308
+ nrdtrisd -- Calculate standard deviation of normal distribution given other params.
309
+ ndtr -- Normal cumulative distribution function.
310
+ log_ndtr -- Logarithm of normal cumulative distribution function.
311
+ ndtri -- Inverse of `ndtr` vs x.
312
+ ndtri_exp -- Inverse of `log_ndtr` vs x.
313
+
314
+ Poisson distribution
315
+ ^^^^^^^^^^^^^^^^^^^^
316
+
317
+ .. autosummary::
318
+ :toctree: generated/
319
+
320
+ pdtr -- Poisson cumulative distribution function.
321
+ pdtrc -- Poisson survival function.
322
+ pdtri -- Inverse to `pdtr` vs m.
323
+ pdtrik -- Inverse to `pdtr` vs k.
324
+
325
+ Student t distribution
326
+ ^^^^^^^^^^^^^^^^^^^^^^
327
+
328
+ .. autosummary::
329
+ :toctree: generated/
330
+
331
+ stdtr -- Student t distribution cumulative distribution function.
332
+ stdtridf -- Inverse of `stdtr` vs df.
333
+ stdtrit -- Inverse of `stdtr` vs `t`.
334
+
335
+ Chi square distribution
336
+ ^^^^^^^^^^^^^^^^^^^^^^^
337
+
338
+ .. autosummary::
339
+ :toctree: generated/
340
+
341
+ chdtr -- Chi square cumulative distribution function.
342
+ chdtrc -- Chi square survival function.
343
+ chdtri -- Inverse to `chdtrc`.
344
+ chdtriv -- Inverse to `chdtr` vs `v`.
345
+
346
+ Non-central chi square distribution
347
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
348
+
349
+ .. autosummary::
350
+ :toctree: generated/
351
+
352
+ chndtr -- Non-central chi square cumulative distribution function.
353
+ chndtridf -- Inverse to `chndtr` vs `df`.
354
+ chndtrinc -- Inverse to `chndtr` vs `nc`.
355
+ chndtrix -- Inverse to `chndtr` vs `x`.
356
+
357
+ Kolmogorov distribution
358
+ ^^^^^^^^^^^^^^^^^^^^^^^
359
+
360
+ .. autosummary::
361
+ :toctree: generated/
362
+
363
+ smirnov -- Kolmogorov-Smirnov complementary cumulative distribution function.
364
+ smirnovi -- Inverse to `smirnov`.
365
+ kolmogorov -- Complementary cumulative distribution function of Kolmogorov distribution.
366
+ kolmogi -- Inverse function to `kolmogorov`.
367
+
368
+ Box-Cox transformation
369
+ ^^^^^^^^^^^^^^^^^^^^^^
370
+
371
+ .. autosummary::
372
+ :toctree: generated/
373
+
374
+ boxcox -- Compute the Box-Cox transformation.
375
+ boxcox1p -- Compute the Box-Cox transformation of 1 + `x`.
376
+ inv_boxcox -- Compute the inverse of the Box-Cox transformation.
377
+ inv_boxcox1p -- Compute the inverse of the Box-Cox transformation.
378
+
379
+
380
+ Sigmoidal functions
381
+ ^^^^^^^^^^^^^^^^^^^
382
+
383
+ .. autosummary::
384
+ :toctree: generated/
385
+
386
+ logit -- Logit ufunc for ndarrays.
387
+ expit -- Logistic sigmoid function.
388
+ log_expit -- Logarithm of the logistic sigmoid function.
389
+
390
+ Miscellaneous
391
+ ^^^^^^^^^^^^^
392
+
393
+ .. autosummary::
394
+ :toctree: generated/
395
+
396
+ tklmbda -- Tukey-Lambda cumulative distribution function.
397
+ owens_t -- Owen's T Function.
398
+
399
+
400
+ Information Theory functions
401
+ ----------------------------
402
+
403
+ .. autosummary::
404
+ :toctree: generated/
405
+
406
+ entr -- Elementwise function for computing entropy.
407
+ rel_entr -- Elementwise function for computing relative entropy.
408
+ kl_div -- Elementwise function for computing Kullback-Leibler divergence.
409
+ huber -- Huber loss function.
410
+ pseudo_huber -- Pseudo-Huber loss function.
411
+
412
+
413
+ Gamma and related functions
414
+ ---------------------------
415
+
416
+ .. autosummary::
417
+ :toctree: generated/
418
+
419
+ gamma -- Gamma function.
420
+ gammaln -- Logarithm of the absolute value of the Gamma function for real inputs.
421
+ loggamma -- Principal branch of the logarithm of the Gamma function.
422
+ gammasgn -- Sign of the gamma function.
423
+ gammainc -- Regularized lower incomplete gamma function.
424
+ gammaincinv -- Inverse to `gammainc`.
425
+ gammaincc -- Regularized upper incomplete gamma function.
426
+ gammainccinv -- Inverse to `gammaincc`.
427
+ beta -- Beta function.
428
+ betaln -- Natural logarithm of absolute value of beta function.
429
+ betainc -- Incomplete beta integral.
430
+ betaincc -- Complemented incomplete beta integral.
431
+ betaincinv -- Inverse function to beta integral.
432
+ betainccinv -- Inverse of the complemented incomplete beta integral.
433
+ psi -- The digamma function.
434
+ rgamma -- Gamma function inverted.
435
+ polygamma -- Polygamma function n.
436
+ multigammaln -- Returns the log of multivariate gamma, also sometimes called the generalized gamma.
437
+ digamma -- psi(x[, out]).
438
+ poch -- Rising factorial (z)_m.
439
+
440
+ Error function and Fresnel integrals
441
+ ------------------------------------
442
+
443
+ .. autosummary::
444
+ :toctree: generated/
445
+
446
+ erf -- Returns the error function of complex argument.
447
+ erfc -- Complementary error function, ``1 - erf(x)``.
448
+ erfcx -- Scaled complementary error function, ``exp(x**2) * erfc(x)``.
449
+ erfi -- Imaginary error function, ``-i erf(i z)``.
450
+ erfinv -- Inverse function for erf.
451
+ erfcinv -- Inverse function for erfc.
452
+ wofz -- Faddeeva function.
453
+ dawsn -- Dawson's integral.
454
+ fresnel -- Fresnel sin and cos integrals.
455
+ fresnel_zeros -- Compute nt complex zeros of sine and cosine Fresnel integrals S(z) and C(z).
456
+ modfresnelp -- Modified Fresnel positive integrals.
457
+ modfresnelm -- Modified Fresnel negative integrals.
458
+ voigt_profile -- Voigt profile.
459
+
460
+ The following functions do not accept NumPy arrays (they are not
461
+ universal functions):
462
+
463
+ .. autosummary::
464
+ :toctree: generated/
465
+
466
+ erf_zeros -- Compute nt complex zeros of error function erf(z).
467
+ fresnelc_zeros -- Compute nt complex zeros of cosine Fresnel integral C(z).
468
+ fresnels_zeros -- Compute nt complex zeros of sine Fresnel integral S(z).
469
+
470
+ Legendre functions
471
+ ------------------
472
+
473
+ .. autosummary::
474
+ :toctree: generated/
475
+
476
+ legendre_p -- Legendre polynomials of the first kind.
477
+ legendre_p_all -- All Legendre polynomials of the first kind up to a specified order.
478
+ assoc_legendre_p -- Associated Legendre polynomials of the first kind.
479
+ assoc_legendre_p_all -- All associated Legendre polynomials of the first kind up to a specified order and degree.
480
+ sph_legendre_p -- Spherical Legendre polynomials of the first kind.
481
+ sph_legendre_p_all -- All spherical Legendre polynomials of the first kind up to a specified order and degree.
482
+ sph_harm_y -- Spherical harmonics.
483
+ sph_harm_y_all -- All spherical harmonics up to a specified order and degree.
484
+
485
+ The following functions are in the process of being deprecated in favor of the above,
486
+ which provide a more flexible and consistent interface.
487
+
488
+ .. autosummary::
489
+ :toctree: generated/
490
+
491
+ lpmv -- Associated Legendre function of integer order and real degree.
492
+ lqn -- Legendre function of the second kind.
493
+ lqmn -- Sequence of associated Legendre functions of the second kind.
494
+
495
+ Ellipsoidal harmonics
496
+ ---------------------
497
+
498
+ .. autosummary::
499
+ :toctree: generated/
500
+
501
+ ellip_harm -- Ellipsoidal harmonic functions E^p_n(l).
502
+ ellip_harm_2 -- Ellipsoidal harmonic functions F^p_n(l).
503
+ ellip_normal -- Ellipsoidal harmonic normalization constants gamma^p_n.
504
+
505
+ Orthogonal polynomials
506
+ ----------------------
507
+
508
+ The following functions evaluate values of orthogonal polynomials:
509
+
510
+ .. autosummary::
511
+ :toctree: generated/
512
+
513
+ assoc_laguerre -- Compute the generalized (associated) Laguerre polynomial of degree n and order k.
514
+ eval_legendre -- Evaluate Legendre polynomial at a point.
515
+ eval_chebyt -- Evaluate Chebyshev polynomial of the first kind at a point.
516
+ eval_chebyu -- Evaluate Chebyshev polynomial of the second kind at a point.
517
+ eval_chebyc -- Evaluate Chebyshev polynomial of the first kind on [-2, 2] at a point.
518
+ eval_chebys -- Evaluate Chebyshev polynomial of the second kind on [-2, 2] at a point.
519
+ eval_jacobi -- Evaluate Jacobi polynomial at a point.
520
+ eval_laguerre -- Evaluate Laguerre polynomial at a point.
521
+ eval_genlaguerre -- Evaluate generalized Laguerre polynomial at a point.
522
+ eval_hermite -- Evaluate physicist's Hermite polynomial at a point.
523
+ eval_hermitenorm -- Evaluate probabilist's (normalized) Hermite polynomial at a point.
524
+ eval_gegenbauer -- Evaluate Gegenbauer polynomial at a point.
525
+ eval_sh_legendre -- Evaluate shifted Legendre polynomial at a point.
526
+ eval_sh_chebyt -- Evaluate shifted Chebyshev polynomial of the first kind at a point.
527
+ eval_sh_chebyu -- Evaluate shifted Chebyshev polynomial of the second kind at a point.
528
+ eval_sh_jacobi -- Evaluate shifted Jacobi polynomial at a point.
529
+
530
+ The following functions compute roots and quadrature weights for
531
+ orthogonal polynomials:
532
+
533
+ .. autosummary::
534
+ :toctree: generated/
535
+
536
+ roots_legendre -- Gauss-Legendre quadrature.
537
+ roots_chebyt -- Gauss-Chebyshev (first kind) quadrature.
538
+ roots_chebyu -- Gauss-Chebyshev (second kind) quadrature.
539
+ roots_chebyc -- Gauss-Chebyshev (first kind) quadrature.
540
+ roots_chebys -- Gauss-Chebyshev (second kind) quadrature.
541
+ roots_jacobi -- Gauss-Jacobi quadrature.
542
+ roots_laguerre -- Gauss-Laguerre quadrature.
543
+ roots_genlaguerre -- Gauss-generalized Laguerre quadrature.
544
+ roots_hermite -- Gauss-Hermite (physicist's) quadrature.
545
+ roots_hermitenorm -- Gauss-Hermite (statistician's) quadrature.
546
+ roots_gegenbauer -- Gauss-Gegenbauer quadrature.
547
+ roots_sh_legendre -- Gauss-Legendre (shifted) quadrature.
548
+ roots_sh_chebyt -- Gauss-Chebyshev (first kind, shifted) quadrature.
549
+ roots_sh_chebyu -- Gauss-Chebyshev (second kind, shifted) quadrature.
550
+ roots_sh_jacobi -- Gauss-Jacobi (shifted) quadrature.
551
+
552
+ The functions below, in turn, return the polynomial coefficients in
553
+ ``orthopoly1d`` objects, which function similarly as `numpy.poly1d`.
554
+ The ``orthopoly1d`` class also has an attribute ``weights``, which returns
555
+ the roots, weights, and total weights for the appropriate form of Gaussian
556
+ quadrature. These are returned in an ``n x 3`` array with roots in the first
557
+ column, weights in the second column, and total weights in the final column.
558
+ Note that ``orthopoly1d`` objects are converted to `~numpy.poly1d` when doing
559
+ arithmetic, and lose information of the original orthogonal polynomial.
560
+
561
+ .. autosummary::
562
+ :toctree: generated/
563
+
564
+ legendre -- Legendre polynomial.
565
+ chebyt -- Chebyshev polynomial of the first kind.
566
+ chebyu -- Chebyshev polynomial of the second kind.
567
+ chebyc -- Chebyshev polynomial of the first kind on :math:`[-2, 2]`.
568
+ chebys -- Chebyshev polynomial of the second kind on :math:`[-2, 2]`.
569
+ jacobi -- Jacobi polynomial.
570
+ laguerre -- Laguerre polynomial.
571
+ genlaguerre -- Generalized (associated) Laguerre polynomial.
572
+ hermite -- Physicist's Hermite polynomial.
573
+ hermitenorm -- Normalized (probabilist's) Hermite polynomial.
574
+ gegenbauer -- Gegenbauer (ultraspherical) polynomial.
575
+ sh_legendre -- Shifted Legendre polynomial.
576
+ sh_chebyt -- Shifted Chebyshev polynomial of the first kind.
577
+ sh_chebyu -- Shifted Chebyshev polynomial of the second kind.
578
+ sh_jacobi -- Shifted Jacobi polynomial.
579
+
580
+ .. warning::
581
+
582
+ Computing values of high-order polynomials (around ``order > 20``) using
583
+ polynomial coefficients is numerically unstable. To evaluate polynomial
584
+ values, the ``eval_*`` functions should be used instead.
585
+
586
+
587
+ Hypergeometric functions
588
+ ------------------------
589
+
590
+ .. autosummary::
591
+ :toctree: generated/
592
+
593
+ hyp2f1 -- Gauss hypergeometric function 2F1(a, b; c; z).
594
+ hyp1f1 -- Confluent hypergeometric function 1F1(a, b; x).
595
+ hyperu -- Confluent hypergeometric function U(a, b, x) of the second kind.
596
+ hyp0f1 -- Confluent hypergeometric limit function 0F1.
597
+
598
+
599
+ Parabolic cylinder functions
600
+ ----------------------------
601
+
602
+ .. autosummary::
603
+ :toctree: generated/
604
+
605
+ pbdv -- Parabolic cylinder function D.
606
+ pbvv -- Parabolic cylinder function V.
607
+ pbwa -- Parabolic cylinder function W.
608
+
609
+ The following functions do not accept NumPy arrays (they are not
610
+ universal functions):
611
+
612
+ .. autosummary::
613
+ :toctree: generated/
614
+
615
+ pbdv_seq -- Parabolic cylinder functions Dv(x) and derivatives.
616
+ pbvv_seq -- Parabolic cylinder functions Vv(x) and derivatives.
617
+ pbdn_seq -- Parabolic cylinder functions Dn(z) and derivatives.
618
+
619
+ Mathieu and related functions
620
+ -----------------------------
621
+
622
+ .. autosummary::
623
+ :toctree: generated/
624
+
625
+ mathieu_a -- Characteristic value of even Mathieu functions.
626
+ mathieu_b -- Characteristic value of odd Mathieu functions.
627
+
628
+ The following functions do not accept NumPy arrays (they are not
629
+ universal functions):
630
+
631
+ .. autosummary::
632
+ :toctree: generated/
633
+
634
+ mathieu_even_coef -- Fourier coefficients for even Mathieu and modified Mathieu functions.
635
+ mathieu_odd_coef -- Fourier coefficients for even Mathieu and modified Mathieu functions.
636
+
637
+ The following return both function and first derivative:
638
+
639
+ .. autosummary::
640
+ :toctree: generated/
641
+
642
+ mathieu_cem -- Even Mathieu function and its derivative.
643
+ mathieu_sem -- Odd Mathieu function and its derivative.
644
+ mathieu_modcem1 -- Even modified Mathieu function of the first kind and its derivative.
645
+ mathieu_modcem2 -- Even modified Mathieu function of the second kind and its derivative.
646
+ mathieu_modsem1 -- Odd modified Mathieu function of the first kind and its derivative.
647
+ mathieu_modsem2 -- Odd modified Mathieu function of the second kind and its derivative.
648
+
649
+ Spheroidal wave functions
650
+ -------------------------
651
+
652
+ .. autosummary::
653
+ :toctree: generated/
654
+
655
+ pro_ang1 -- Prolate spheroidal angular function of the first kind and its derivative.
656
+ pro_rad1 -- Prolate spheroidal radial function of the first kind and its derivative.
657
+ pro_rad2 -- Prolate spheroidal radial function of the second kind and its derivative.
658
+ obl_ang1 -- Oblate spheroidal angular function of the first kind and its derivative.
659
+ obl_rad1 -- Oblate spheroidal radial function of the first kind and its derivative.
660
+ obl_rad2 -- Oblate spheroidal radial function of the second kind and its derivative.
661
+ pro_cv -- Characteristic value of prolate spheroidal function.
662
+ obl_cv -- Characteristic value of oblate spheroidal function.
663
+ pro_cv_seq -- Characteristic values for prolate spheroidal wave functions.
664
+ obl_cv_seq -- Characteristic values for oblate spheroidal wave functions.
665
+
666
+ The following functions require pre-computed characteristic value:
667
+
668
+ .. autosummary::
669
+ :toctree: generated/
670
+
671
+ pro_ang1_cv -- Prolate spheroidal angular function pro_ang1 for precomputed characteristic value.
672
+ pro_rad1_cv -- Prolate spheroidal radial function pro_rad1 for precomputed characteristic value.
673
+ pro_rad2_cv -- Prolate spheroidal radial function pro_rad2 for precomputed characteristic value.
674
+ obl_ang1_cv -- Oblate spheroidal angular function obl_ang1 for precomputed characteristic value.
675
+ obl_rad1_cv -- Oblate spheroidal radial function obl_rad1 for precomputed characteristic value.
676
+ obl_rad2_cv -- Oblate spheroidal radial function obl_rad2 for precomputed characteristic value.
677
+
678
+ Kelvin functions
679
+ ----------------
680
+
681
+ .. autosummary::
682
+ :toctree: generated/
683
+
684
+ kelvin -- Kelvin functions as complex numbers.
685
+ kelvin_zeros -- Compute nt zeros of all Kelvin functions.
686
+ ber -- Kelvin function ber.
687
+ bei -- Kelvin function bei
688
+ berp -- Derivative of the Kelvin function `ber`.
689
+ beip -- Derivative of the Kelvin function `bei`.
690
+ ker -- Kelvin function ker.
691
+ kei -- Kelvin function ker.
692
+ kerp -- Derivative of the Kelvin function ker.
693
+ keip -- Derivative of the Kelvin function kei.
694
+
695
+ The following functions do not accept NumPy arrays (they are not
696
+ universal functions):
697
+
698
+ .. autosummary::
699
+ :toctree: generated/
700
+
701
+ ber_zeros -- Compute nt zeros of the Kelvin function ber(x).
702
+ bei_zeros -- Compute nt zeros of the Kelvin function bei(x).
703
+ berp_zeros -- Compute nt zeros of the Kelvin function ber'(x).
704
+ beip_zeros -- Compute nt zeros of the Kelvin function bei'(x).
705
+ ker_zeros -- Compute nt zeros of the Kelvin function ker(x).
706
+ kei_zeros -- Compute nt zeros of the Kelvin function kei(x).
707
+ kerp_zeros -- Compute nt zeros of the Kelvin function ker'(x).
708
+ keip_zeros -- Compute nt zeros of the Kelvin function kei'(x).
709
+
710
+ Combinatorics
711
+ -------------
712
+
713
+ .. autosummary::
714
+ :toctree: generated/
715
+
716
+ comb -- The number of combinations of N things taken k at a time.
717
+ perm -- Permutations of N things taken k at a time, i.e., k-permutations of N.
718
+ stirling2 -- Stirling numbers of the second kind.
719
+
720
+ Lambert W and related functions
721
+ -------------------------------
722
+
723
+ .. autosummary::
724
+ :toctree: generated/
725
+
726
+ lambertw -- Lambert W function.
727
+ wrightomega -- Wright Omega function.
728
+
729
+ Other special functions
730
+ -----------------------
731
+
732
+ .. autosummary::
733
+ :toctree: generated/
734
+
735
+ agm -- Arithmetic, Geometric Mean.
736
+ bernoulli -- Bernoulli numbers B0..Bn (inclusive).
737
+ binom -- Binomial coefficient
738
+ diric -- Periodic sinc function, also called the Dirichlet kernel.
739
+ euler -- Euler numbers E0..En (inclusive).
740
+ expn -- Exponential integral E_n.
741
+ exp1 -- Exponential integral E_1 of complex argument z.
742
+ expi -- Exponential integral Ei.
743
+ factorial -- The factorial of a number or array of numbers.
744
+ factorial2 -- Double factorial.
745
+ factorialk -- Multifactorial of n of order k, n(!!...!).
746
+ shichi -- Hyperbolic sine and cosine integrals.
747
+ sici -- Sine and cosine integrals.
748
+ softmax -- Softmax function.
749
+ log_softmax -- Logarithm of softmax function.
750
+ spence -- Spence's function, also known as the dilogarithm.
751
+ zeta -- Riemann zeta function.
752
+ zetac -- Riemann zeta function minus 1.
753
+ softplus -- Softplus function.
754
+
755
+ Convenience functions
756
+ ---------------------
757
+
758
+ .. autosummary::
759
+ :toctree: generated/
760
+
761
+ cbrt -- Cube root of `x`.
762
+ exp10 -- 10**x.
763
+ exp2 -- 2**x.
764
+ radian -- Convert from degrees to radians.
765
+ cosdg -- Cosine of the angle `x` given in degrees.
766
+ sindg -- Sine of angle given in degrees.
767
+ tandg -- Tangent of angle x given in degrees.
768
+ cotdg -- Cotangent of the angle `x` given in degrees.
769
+ log1p -- Calculates log(1+x) for use when `x` is near zero.
770
+ expm1 -- ``exp(x) - 1`` for use when `x` is near zero.
771
+ cosm1 -- ``cos(x) - 1`` for use when `x` is near zero.
772
+ powm1 -- ``x**y - 1`` for use when `y` is near zero or `x` is near 1.
773
+ round -- Round to nearest integer.
774
+ xlogy -- Compute ``x*log(y)`` so that the result is 0 if ``x = 0``.
775
+ xlog1py -- Compute ``x*log1p(y)`` so that the result is 0 if ``x = 0``.
776
+ logsumexp -- Compute the log of the sum of exponentials of input elements.
777
+ exprel -- Relative error exponential, (exp(x)-1)/x, for use when `x` is near zero.
778
+ sinc -- Return the sinc function.
779
+
780
+ """ # noqa: E501
781
+
782
+
783
+ from ._sf_error import SpecialFunctionWarning, SpecialFunctionError
784
+
785
+ from . import _ufuncs
786
+ from ._ufuncs import *
787
+
788
+ from . import _basic
789
+ from ._basic import *
790
+
791
+ # Replace some function definitions from _ufuncs and _basic
792
+ # to add Array API support
793
+ from ._support_alternative_backends import *
794
+
795
+ from ._logsumexp import logsumexp, softmax, log_softmax
796
+
797
+ from . import _multiufuncs
798
+ from ._multiufuncs import *
799
+
800
+ from . import _orthogonal
801
+ from ._orthogonal import *
802
+
803
+ from ._ellip_harm import (
804
+ ellip_harm,
805
+ ellip_harm_2,
806
+ ellip_normal
807
+ )
808
+ from ._lambertw import lambertw
809
+ from ._spherical_bessel import (
810
+ spherical_jn,
811
+ spherical_yn,
812
+ spherical_in,
813
+ spherical_kn
814
+ )
815
+
816
+ # Deprecated namespaces, to be removed in v2.0.0
817
+ from . import add_newdocs, basic, orthogonal, specfun, sf_error, spfun_stats
818
+
819
+ # We replace some function definitions from _ufuncs with those from
820
+ # _support_alternative_backends above, but those are all listed in _ufuncs.__all__,
821
+ # so there is no need to consider _support_alternative_backends.__all__ here.
822
+ __all__ = _ufuncs.__all__ + _basic.__all__ + _orthogonal.__all__ + _multiufuncs.__all__
823
+ __all__ += [
824
+ 'SpecialFunctionWarning',
825
+ 'SpecialFunctionError',
826
+ 'logsumexp',
827
+ 'softmax',
828
+ 'log_softmax',
829
+ 'multigammaln', # pyrefly:ignore[bad-dunder-all]
830
+ 'ellip_harm',
831
+ 'ellip_harm_2',
832
+ 'ellip_normal',
833
+ 'lambertw',
834
+ 'spherical_jn',
835
+ 'spherical_yn',
836
+ 'spherical_in',
837
+ 'spherical_kn',
838
+ ]
839
+
840
+ from scipy._lib._testutils import PytestTester
841
+ test = PytestTester(__name__)
842
+ del PytestTester
v12x/pylib/scipy/special/_add_newdocs.py ADDED
The diff for this file is too large to render. See raw diff
 
v12x/pylib/scipy/special/_basic.py ADDED
The diff for this file is too large to render. See raw diff
 
v12x/pylib/scipy/special/_ellip_harm.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from ._ufuncs import _ellip_harm
4
+ from ._ellip_harm_2 import _ellipsoid, _ellipsoid_norm
5
+
6
+
7
+ def ellip_harm(h2, k2, n, p, s, signm=1, signn=1):
8
+ r"""
9
+ Ellipsoidal harmonic functions E^p_n(l).
10
+
11
+ These are also known as Lamé functions of the first kind, and are
12
+ solutions to the Lamé equation:
13
+
14
+ .. math:: (s^2 - h^2)(s^2 - k^2)E''(s)
15
+ + s(2s^2 - h^2 - k^2)E'(s) + (a - q s^2)E(s) = 0
16
+
17
+ where :math:`q = (n+1)n` and :math:`a` is the eigenvalue (not
18
+ returned) corresponding to the solutions.
19
+
20
+ Parameters
21
+ ----------
22
+ h2 : float
23
+ ``h**2``
24
+ k2 : float
25
+ ``k**2``; should be larger than ``h**2``
26
+ n : int
27
+ Degree
28
+ p : int
29
+ Order, can range between ``[1, 2n+1]``
30
+ s : float
31
+ Coordinate
32
+ signm : {1, -1}, optional
33
+ Sign of prefactor of functions. Can be +/-1. See Notes.
34
+ signn : {1, -1}, optional
35
+ Sign of prefactor of functions. Can be +/-1. See Notes.
36
+
37
+ Returns
38
+ -------
39
+ E : float
40
+ the harmonic :math:`E^p_n(s)`
41
+
42
+ See Also
43
+ --------
44
+ ellip_harm_2, ellip_normal
45
+
46
+ Notes
47
+ -----
48
+ The geometric interpretation of the ellipsoidal functions is
49
+ explained in [2]_, [3]_, [4]_. The `signm` and `signn` arguments control the
50
+ sign of prefactors for functions according to their type::
51
+
52
+ K : +1
53
+ L : signm
54
+ M : signn
55
+ N : signm*signn
56
+
57
+ .. versionadded:: 0.15.0
58
+
59
+ References
60
+ ----------
61
+ .. [1] Digital Library of Mathematical Functions 29.12
62
+ https://dlmf.nist.gov/29.12
63
+ .. [2] Bardhan and Knepley, "Computational science and
64
+ re-discovery: open-source implementations of
65
+ ellipsoidal harmonics for problems in potential theory",
66
+ Comput. Sci. Disc. 5, 014006 (2012)
67
+ :doi:`10.1088/1749-4699/5/1/014006`.
68
+ .. [3] David J.and Dechambre P, "Computation of Ellipsoidal
69
+ Gravity Field Harmonics for small solar system bodies"
70
+ pp. 30-36, 2000
71
+ .. [4] George Dassios, "Ellipsoidal Harmonics: Theory and Applications"
72
+ pp. 418, 2012
73
+
74
+ Examples
75
+ --------
76
+ >>> from scipy.special import ellip_harm
77
+ >>> w = ellip_harm(5,8,1,1,2.5)
78
+ >>> w
79
+ 2.5
80
+
81
+ Check that the functions indeed are solutions to the Lamé equation:
82
+
83
+ >>> import numpy as np
84
+ >>> from scipy.interpolate import UnivariateSpline
85
+ >>> def eigenvalue(f, df, ddf):
86
+ ... r = (((s**2 - h**2) * (s**2 - k**2) * ddf
87
+ ... + s * (2*s**2 - h**2 - k**2) * df
88
+ ... - n * (n + 1)*s**2*f) / f)
89
+ ... return -r.mean(), r.std()
90
+ >>> s = np.linspace(0.1, 10, 200)
91
+ >>> k, h, n, p = 8.0, 2.2, 3, 2
92
+ >>> E = ellip_harm(h**2, k**2, n, p, s)
93
+ >>> E_spl = UnivariateSpline(s, E)
94
+ >>> a, a_err = eigenvalue(E_spl(s), E_spl(s,1), E_spl(s,2))
95
+ >>> a, a_err
96
+ (583.44366156701483, 6.4580890640310646e-11)
97
+
98
+ """ # noqa: E501
99
+ return _ellip_harm(h2, k2, n, p, s, signm, signn)
100
+
101
+
102
+ _ellip_harm_2_vec = np.vectorize(_ellipsoid, otypes='d')
103
+
104
+
105
+ def ellip_harm_2(h2, k2, n, p, s):
106
+ r"""
107
+ Ellipsoidal harmonic functions F^p_n(l).
108
+
109
+ These are also known as Lamé functions of the second kind, and are
110
+ solutions to the Lamé equation:
111
+
112
+ .. math:: (s^2 - h^2)(s^2 - k^2)F''(s)
113
+ + s(2s^2 - h^2 - k^2)F'(s) + (a - q s^2)F(s) = 0
114
+
115
+ where :math:`q = (n+1)n` and :math:`a` is the eigenvalue (not
116
+ returned) corresponding to the solutions.
117
+
118
+ Parameters
119
+ ----------
120
+ h2 : float
121
+ ``h**2``
122
+ k2 : float
123
+ ``k**2``; should be larger than ``h**2``
124
+ n : int
125
+ Degree.
126
+ p : int
127
+ Order, can range between [1,2n+1].
128
+ s : float
129
+ Coordinate
130
+
131
+ Returns
132
+ -------
133
+ F : float
134
+ The harmonic :math:`F^p_n(s)`
135
+
136
+ See Also
137
+ --------
138
+ ellip_harm, ellip_normal
139
+
140
+ Notes
141
+ -----
142
+ Lamé functions of the second kind are related to the functions of the first kind:
143
+
144
+ .. math::
145
+
146
+ F^p_n(s)=(2n + 1)E^p_n(s)\int_{0}^{1/s}
147
+ \frac{du}{(E^p_n(1/u))^2\sqrt{(1-u^2k^2)(1-u^2h^2)}}
148
+
149
+ .. versionadded:: 0.15.0
150
+
151
+ Examples
152
+ --------
153
+ >>> from scipy.special import ellip_harm_2
154
+ >>> w = ellip_harm_2(5,8,2,1,10)
155
+ >>> w
156
+ 0.00108056853382
157
+
158
+ """
159
+ with np.errstate(all='ignore'):
160
+ return _ellip_harm_2_vec(h2, k2, n, p, s)
161
+
162
+
163
+ def _ellip_normal_vec(h2, k2, n, p):
164
+ return _ellipsoid_norm(h2, k2, n, p)
165
+
166
+
167
+ _ellip_normal_vec = np.vectorize(_ellip_normal_vec, otypes='d')
168
+
169
+
170
+ def ellip_normal(h2, k2, n, p):
171
+ r"""
172
+ Ellipsoidal harmonic normalization constants gamma^p_n.
173
+
174
+ The normalization constant is defined as
175
+
176
+ .. math::
177
+
178
+ \gamma^p_n=8\int_{0}^{h}dx\int_{h}^{k}dy
179
+ \frac{(y^2-x^2)(E^p_n(y)E^p_n(x))^2}{\sqrt((k^2-y^2)(y^2-h^2)(h^2-x^2)(k^2-x^2)}
180
+
181
+ Parameters
182
+ ----------
183
+ h2 : float
184
+ ``h**2``
185
+ k2 : float
186
+ ``k**2``; should be larger than ``h**2``
187
+ n : int
188
+ Degree.
189
+ p : int
190
+ Order, can range between [1,2n+1].
191
+
192
+ Returns
193
+ -------
194
+ gamma : float
195
+ The normalization constant :math:`\gamma^p_n`
196
+
197
+ See Also
198
+ --------
199
+ ellip_harm, ellip_harm_2
200
+
201
+ Notes
202
+ -----
203
+ .. versionadded:: 0.15.0
204
+
205
+ Examples
206
+ --------
207
+ >>> from scipy.special import ellip_normal
208
+ >>> w = ellip_normal(5,8,3,7)
209
+ >>> w
210
+ 1723.38796997
211
+
212
+ """
213
+ with np.errstate(all='ignore'):
214
+ return _ellip_normal_vec(h2, k2, n, p)
v12x/pylib/scipy/special/_input_validation.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import operator
3
+
4
+ def _nonneg_int_or_fail(n, var_name, strict=True):
5
+ try:
6
+ if strict:
7
+ # Raises an exception if float
8
+ n = operator.index(n)
9
+ elif n == math.floor(n):
10
+ n = int(n)
11
+ else:
12
+ raise ValueError()
13
+ if n < 0:
14
+ raise ValueError()
15
+ except (ValueError, TypeError) as err:
16
+ raise err.__class__(f"{var_name} must be a non-negative integer") from err
17
+ return n
v12x/pylib/scipy/special/_lambertw.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ._ufuncs import _lambertw
2
+
3
+ import numpy as np
4
+
5
+
6
+ def lambertw(z, k=0, tol=1e-8):
7
+ r"""
8
+ lambertw(z, k=0, tol=1e-8)
9
+
10
+ Lambert W function.
11
+
12
+ The Lambert W function `W(z)` is defined as the inverse function
13
+ of ``w * exp(w)``. In other words, the value of ``W(z)`` is
14
+ such that ``z = W(z) * exp(W(z))`` for any complex number
15
+ ``z``.
16
+
17
+ The Lambert W function is a multivalued function with infinitely
18
+ many branches. Each branch gives a separate solution of the
19
+ equation ``z = w exp(w)``. Here, the branches are indexed by the
20
+ integer `k`.
21
+
22
+ Parameters
23
+ ----------
24
+ z : array_like
25
+ Input argument.
26
+ k : int, optional
27
+ Branch index.
28
+ tol : float, optional
29
+ Evaluation tolerance.
30
+
31
+ Returns
32
+ -------
33
+ w : array
34
+ `w` will have the same shape as `z`.
35
+
36
+ See Also
37
+ --------
38
+ wrightomega : the Wright Omega function
39
+
40
+ Notes
41
+ -----
42
+ All branches are supported by `lambertw`:
43
+
44
+ * ``lambertw(z)`` gives the principal solution (branch 0)
45
+ * ``lambertw(z, k)`` gives the solution on branch `k`
46
+
47
+ The Lambert W function has two partially real branches: the
48
+ principal branch (`k = 0`) is real for real ``z > -1/e``, and the
49
+ ``k = -1`` branch is real for ``-1/e < z < 0``. All branches except
50
+ ``k = 0`` have a logarithmic singularity at ``z = 0``.
51
+
52
+ **Possible issues**
53
+
54
+ The evaluation can become inaccurate very close to the branch point
55
+ at ``-1/e``. In some corner cases, `lambertw` might currently
56
+ fail to converge, or can end up on the wrong branch.
57
+
58
+ **Algorithm**
59
+
60
+ Halley's iteration is used to invert ``w * exp(w)``, using a first-order
61
+ asymptotic approximation (O(log(w)) or `O(w)`) as the initial estimate.
62
+
63
+ The definition, implementation and choice of branches is based on [2]_.
64
+
65
+ References
66
+ ----------
67
+ .. [1] https://en.wikipedia.org/wiki/Lambert_W_function
68
+ .. [2] Corless et al, "On the Lambert W function", Adv. Comp. Math. 5
69
+ (1996) 329-359.
70
+ https://cs.uwaterloo.ca/research/tr/1993/03/W.pdf
71
+
72
+ Examples
73
+ --------
74
+ The Lambert W function is the inverse of ``w exp(w)``:
75
+
76
+ >>> import numpy as np
77
+ >>> from scipy.special import lambertw
78
+ >>> w = lambertw(1)
79
+ >>> w
80
+ (0.56714329040978384+0j)
81
+ >>> w * np.exp(w)
82
+ (1.0+0j)
83
+
84
+ Any branch gives a valid inverse:
85
+
86
+ >>> w = lambertw(1, k=3)
87
+ >>> w
88
+ (-2.8535817554090377+17.113535539412148j)
89
+ >>> w*np.exp(w)
90
+ (1.0000000000000002+1.609823385706477e-15j)
91
+
92
+ **Applications to equation-solving**
93
+
94
+ The Lambert W function may be used to solve various kinds of
95
+ equations. We give two examples here.
96
+
97
+ First, the function can be used to solve implicit equations of the
98
+ form
99
+
100
+ :math:`x = a + b e^{c x}`
101
+
102
+ for :math:`x`. We assume :math:`c` is not zero. After a little
103
+ algebra, the equation may be written
104
+
105
+ :math:`z e^z = -b c e^{a c}`
106
+
107
+ where :math:`z = c (a - x)`. :math:`z` may then be expressed using
108
+ the Lambert W function
109
+
110
+ :math:`z = W(-b c e^{a c})`
111
+
112
+ giving
113
+
114
+ :math:`x = a - W(-b c e^{a c})/c`
115
+
116
+ For example,
117
+
118
+ >>> a = 3
119
+ >>> b = 2
120
+ >>> c = -0.5
121
+
122
+ The solution to :math:`x = a + b e^{c x}` is:
123
+
124
+ >>> x = a - lambertw(-b*c*np.exp(a*c))/c
125
+ >>> x
126
+ (3.3707498368978794+0j)
127
+
128
+ Verify that it solves the equation:
129
+
130
+ >>> a + b*np.exp(c*x)
131
+ (3.37074983689788+0j)
132
+
133
+ The Lambert W function may also be used find the value of the infinite
134
+ power tower :math:`z^{z^{z^{\ldots}}}`:
135
+
136
+ >>> def tower(z, n):
137
+ ... if n == 0:
138
+ ... return z
139
+ ... return z ** tower(z, n-1)
140
+ ...
141
+ >>> tower(0.5, 100)
142
+ 0.641185744504986
143
+ >>> -lambertw(-np.log(0.5)) / np.log(0.5)
144
+ (0.64118574450498589+0j)
145
+ """
146
+ # TODO: special expert should inspect this
147
+ # interception; better place to do it?
148
+ k = np.asarray(k, dtype=np.dtype("long"))
149
+ return _lambertw(z, k, tol)
v12x/pylib/scipy/special/_logsumexp.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from scipy._lib._array_api import (
3
+ array_namespace,
4
+ xp_capabilities,
5
+ xp_device,
6
+ xp_size,
7
+ xp_promote,
8
+ xp_float_to_complex,
9
+ )
10
+ from scipy._external import array_api_extra as xpx
11
+
12
+ __all__ = ["logsumexp", "softmax", "log_softmax"]
13
+
14
+
15
+ @xp_capabilities()
16
+ def logsumexp(a, axis=None, b=None, keepdims=False, return_sign=False):
17
+ """Compute the log of the sum of exponentials of input elements.
18
+
19
+ Parameters
20
+ ----------
21
+ a : array_like
22
+ Input array.
23
+ axis : None or int or tuple of ints, optional
24
+ Axis or axes over which the sum is taken. By default `axis` is None,
25
+ and all elements are summed.
26
+
27
+ .. versionadded:: 0.11.0
28
+ b : array-like, optional
29
+ Scaling factor for exp(`a`) must be of the same shape as `a` or
30
+ broadcastable to `a`. These values may be negative in order to
31
+ implement subtraction.
32
+
33
+ .. versionadded:: 0.12.0
34
+ keepdims : bool, optional
35
+ If this is set to True, the axes which are reduced are left in the
36
+ result as dimensions with size one. With this option, the result
37
+ will broadcast correctly against the original array.
38
+
39
+ .. versionadded:: 0.15.0
40
+ return_sign : bool, optional
41
+ If this is set to True, the result will be a pair containing sign
42
+ information; if False, results that are negative will be returned
43
+ as NaN. Default is False (no sign information).
44
+
45
+ .. versionadded:: 0.16.0
46
+
47
+ Returns
48
+ -------
49
+ res : ndarray
50
+ The result, ``np.log(np.sum(np.exp(a)))`` calculated in a numerically
51
+ more stable way. If `b` is given then ``np.log(np.sum(b*np.exp(a)))``
52
+ is returned. If ``return_sign`` is True, ``res`` contains the log of
53
+ the absolute value of the argument.
54
+ sgn : ndarray
55
+ If ``return_sign`` is True, this will be an array of floating-point
56
+ numbers matching res containing +1, 0, -1 (for real-valued inputs)
57
+ or a complex phase (for complex inputs). This gives the sign of the
58
+ argument of the logarithm in ``res``.
59
+ If ``return_sign`` is False, only one result is returned.
60
+
61
+ See Also
62
+ --------
63
+ :data:`numpy.logaddexp`
64
+ :data:`numpy.logaddexp2`
65
+
66
+ Notes
67
+ -----
68
+ NumPy has a logaddexp function which is very similar to `logsumexp`, but
69
+ only handles two arguments. `logaddexp.reduce` is similar to this
70
+ function, but may be less stable.
71
+
72
+ The logarithm is a multivalued function: for each :math:`x` there is an
73
+ infinite number of :math:`z` such that :math:`exp(z) = x`. The convention
74
+ is to return the :math:`z` whose imaginary part lies in :math:`(-pi, pi]`.
75
+
76
+ Examples
77
+ --------
78
+ >>> import numpy as np
79
+ >>> from scipy.special import logsumexp
80
+ >>> a = np.arange(10)
81
+ >>> logsumexp(a)
82
+ 9.4586297444267107
83
+ >>> np.log(np.sum(np.exp(a)))
84
+ 9.4586297444267107
85
+
86
+ With weights
87
+
88
+ >>> a = np.arange(10)
89
+ >>> b = np.arange(10, 0, -1)
90
+ >>> logsumexp(a, b=b)
91
+ 9.9170178533034665
92
+ >>> np.log(np.sum(b*np.exp(a)))
93
+ 9.9170178533034647
94
+
95
+ Returning a sign flag
96
+
97
+ >>> logsumexp([1,2],b=[1,-1],return_sign=True)
98
+ (1.5413248546129181, -1.0)
99
+
100
+ Notice that `logsumexp` does not directly support masked arrays. To use it
101
+ on a masked array, convert the mask into zero weights:
102
+
103
+ >>> a = np.ma.array([np.log(2), 2, np.log(3)],
104
+ ... mask=[False, True, False])
105
+ >>> b = (~a.mask).astype(int)
106
+ >>> logsumexp(a.data, b=b), np.log(5)
107
+ 1.6094379124341005, 1.6094379124341005
108
+
109
+ """
110
+ xp = array_namespace(a, b)
111
+ a, b = xp_promote(a, b, broadcast=True, force_floating=True, xp=xp)
112
+ a = xpx.atleast_nd(a, ndim=1, xp=xp)
113
+ b = xpx.atleast_nd(b, ndim=1, xp=xp) if b is not None else b
114
+ axis = tuple(range(a.ndim)) if axis is None else axis
115
+
116
+ if xp_size(a) != 0:
117
+ with np.errstate(divide='ignore', invalid='ignore', over='ignore'):
118
+ # Where result is infinite, we use the direct logsumexp calculation to
119
+ # delegate edge case handling to the behavior of `xp.log` and `xp.exp`,
120
+ # which should follow the C99 standard for complex values.
121
+ b_exp_a = xp.exp(a) if b is None else b * xp.exp(a)
122
+ sum_ = xp.sum(b_exp_a, axis=axis, keepdims=True)
123
+ sgn_inf = xp.sign(sum_) if return_sign else None
124
+ sum_ = xp.abs(sum_) if return_sign else sum_
125
+ out_inf = xp.log(sum_)
126
+
127
+ with np.errstate(divide='ignore', invalid='ignore'): # log of zero is OK
128
+ out, sgn = _logsumexp(a, b, axis=axis, return_sign=return_sign, xp=xp)
129
+
130
+ # Replace infinite results. This probably could be done with an
131
+ # `apply_where`-like strategy to avoid redundant calculation, but currently
132
+ # `apply_where` itself is only for elementwise functions.
133
+ out_finite = xp.isfinite(out)
134
+ out = xp.where(out_finite, out, out_inf)
135
+ sgn = xp.where(out_finite, sgn, sgn_inf) if return_sign else sgn
136
+ else:
137
+ shape = np.asarray(a.shape) # NumPy is convenient for shape manipulation
138
+ shape[axis] = 1
139
+ out = xp.full(tuple(shape), -xp.inf, dtype=a.dtype, device=xp_device(a))
140
+ sgn = xp.sign(out)
141
+
142
+ if xp.isdtype(out.dtype, 'complex floating'):
143
+ if return_sign:
144
+ real = xp.real(sgn)
145
+ imag = xp_float_to_complex(_wrap_radians(xp.imag(sgn), xp=xp), xp=xp)
146
+ sgn = real + imag*1j
147
+ else:
148
+ real = xp.real(out)
149
+ imag = xp_float_to_complex(_wrap_radians(xp.imag(out), xp=xp), xp=xp)
150
+ out = real + imag*1j
151
+
152
+ # Deal with shape details - reducing dimensions and convert 0-D to scalar for NumPy
153
+ out = xp.squeeze(out, axis=axis) if not keepdims else out
154
+ sgn = xp.squeeze(sgn, axis=axis) if (sgn is not None and not keepdims) else sgn
155
+ out = out[()] if out.ndim == 0 else out
156
+ sgn = sgn[()] if (sgn is not None and sgn.ndim == 0) else sgn
157
+
158
+ return (out, sgn) if return_sign else out
159
+
160
+
161
+ def _wrap_radians(x, *, xp):
162
+ # Wrap radians to (-pi, pi] interval
163
+ wrapped = -((-x + xp.pi) % (2 * xp.pi) - xp.pi)
164
+ # preserve relative precision
165
+ no_wrap = xp.abs(x) < xp.pi
166
+ return xp.where(no_wrap, x, wrapped)
167
+
168
+
169
+ def _elements_and_indices_with_max_real(a, *, axis=-1, xp):
170
+ # This is an array-API compatible `max` function that works something
171
+ # like `np.max` for complex input. The important part is that it finds
172
+ # the element with maximum real part. When there are multiple complex values
173
+ # with this real part, it doesn't matter which we choose.
174
+ # We could use `argmax` on real component, but array API doesn't yet have
175
+ # `take_along_axis`, and even if it did, we would have problems with axis tuples.
176
+ # Feel free to rewrite! It's ugly, but it's not the purpose of the PR, and
177
+ # it gets the job done.
178
+
179
+ if xp.isdtype(a.dtype, "complex floating"):
180
+ # select all elements with max real part.
181
+ real_a = xp.real(a)
182
+ max_ = xp.max(real_a, axis=axis, keepdims=True)
183
+ mask = real_a == max_
184
+
185
+ # Of those, choose one arbitrarily. This is a reasonably
186
+ # simple, array-API compatible way of doing so that doesn't
187
+ # have a problem with `axis` being a tuple or None.
188
+ i = xp.reshape(xp.arange(xp_size(a), device=xp_device(a)), a.shape)
189
+ i = xpx.at(i, ~mask).set(-1)
190
+ max_i = xp.max(i, axis=axis, keepdims=True)
191
+ mask = i == max_i
192
+ a = xp.where(mask, a, 0.)
193
+ max_ = xp.sum(a, axis=axis, dtype=a.dtype, keepdims=True)
194
+ else:
195
+ max_ = xp.max(a, axis=axis, keepdims=True)
196
+ mask = a == max_
197
+
198
+ return max_, mask
199
+
200
+
201
+ def _logsumexp(a, b, *, axis, return_sign, xp):
202
+ # This has been around for about a decade, so let's consider it a feature:
203
+ # Even if element of `a` is infinite or NaN, it adds nothing to the sum if
204
+ # the corresponding weight is zero.
205
+ if b is not None:
206
+ a = xpx.at(a, b == 0).set(-xp.inf, copy=True)
207
+
208
+ # Find element with maximum real part, since this is what affects the magnitude
209
+ # of the exponential. Possible enhancement: include log of `b` magnitude in `a`.
210
+ a_max, i_max = _elements_and_indices_with_max_real(a, axis=axis, xp=xp)
211
+
212
+ # for precision, these terms are separated out of the main sum.
213
+ a = xpx.at(a, i_max).set(-xp.inf, copy=True if b is None else None)
214
+ i_max_dt = xp.astype(i_max, a.dtype)
215
+ # This is an inefficient way of getting `m` because it is the sum of a sparse
216
+ # array; however, this is the simplest way I can think of to get the right shape.
217
+ b_i_max = i_max_dt if b is None else b * i_max_dt
218
+ m = xp.sum(b_i_max, axis=axis, keepdims=True, dtype=a.dtype)
219
+
220
+ # Shift, exponentiate, scale, and sum
221
+ exp = b * xp.exp(a - a_max) if b is not None else xp.exp(a - a_max)
222
+ s = xp.sum(exp, axis=axis, keepdims=True, dtype=exp.dtype)
223
+ s = xp.where(s == 0, s, s/m)
224
+
225
+ # Separate sign/magnitude information
226
+ # Originally, this was only performed if `return_sign=True`.
227
+ # However, this is also needed if any elements of `m < 0` or `s < -1`.
228
+ # An improvement would be to perform the calculations only on these entries.
229
+
230
+ sgn = xp.sign(s + 1) * xp.sign(m)
231
+
232
+ if xp.isdtype(s.dtype, "real floating"):
233
+ # The log functions need positive arguments
234
+ s = xp.where(s < -1, -s - 2, s)
235
+ m = xp.abs(m)
236
+ else:
237
+ # `a_max` can have a sign component for complex input
238
+ sgn = sgn * xp.exp(xp.imag(a_max) * 1.0j)
239
+
240
+ # Take log and undo shift
241
+ out = xp.log1p(s) + xp.log(m) + a_max
242
+
243
+ if return_sign:
244
+ out = xp.real(out)
245
+ elif xp.isdtype(out.dtype, 'real floating'):
246
+ out = xpx.at(out)[sgn < 0].set(xp.nan)
247
+
248
+ return out, sgn
249
+
250
+
251
+ @xp_capabilities()
252
+ def softmax(x, axis=None):
253
+ r"""Compute the softmax function.
254
+
255
+ The softmax function transforms each element of a collection by
256
+ computing the exponential of each element divided by the sum of the
257
+ exponentials of all the elements. That is, if `x` is a one-dimensional
258
+ numpy array::
259
+
260
+ softmax(x) = np.exp(x)/sum(np.exp(x))
261
+
262
+ Parameters
263
+ ----------
264
+ x : array_like
265
+ Input array.
266
+ axis : int or tuple of ints, optional
267
+ Axis to compute values along. Default is None and softmax will be
268
+ computed over the entire array `x`.
269
+
270
+ Returns
271
+ -------
272
+ s : ndarray
273
+ An array the same shape as `x`. The result will sum to 1 along the
274
+ specified axis.
275
+
276
+ Notes
277
+ -----
278
+ The formula for the softmax function :math:`\sigma(x)` for a vector
279
+ :math:`x = \{x_0, x_1, ..., x_{n-1}\}` is
280
+
281
+ .. math:: \sigma(x)_j = \frac{e^{x_j}}{\sum_k e^{x_k}}
282
+
283
+ The `softmax` function is the gradient of `logsumexp`.
284
+
285
+ The implementation uses shifting to avoid overflow. See [1]_ for more
286
+ details.
287
+
288
+ .. versionadded:: 1.2.0
289
+
290
+ References
291
+ ----------
292
+ .. [1] P. Blanchard, D.J. Higham, N.J. Higham, "Accurately computing the
293
+ log-sum-exp and softmax functions", IMA Journal of Numerical Analysis,
294
+ Vol.41(4), :doi:`10.1093/imanum/draa038`.
295
+
296
+ Examples
297
+ --------
298
+ >>> import numpy as np
299
+ >>> from scipy.special import softmax
300
+ >>> np.set_printoptions(precision=5)
301
+
302
+ >>> x = np.array([[1, 0.5, 0.2, 3],
303
+ ... [1, -1, 7, 3],
304
+ ... [2, 12, 13, 3]])
305
+ ...
306
+
307
+ Compute the softmax transformation over the entire array.
308
+
309
+ >>> m = softmax(x)
310
+ >>> m
311
+ array([[ 4.48309e-06, 2.71913e-06, 2.01438e-06, 3.31258e-05],
312
+ [ 4.48309e-06, 6.06720e-07, 1.80861e-03, 3.31258e-05],
313
+ [ 1.21863e-05, 2.68421e-01, 7.29644e-01, 3.31258e-05]])
314
+
315
+ >>> m.sum()
316
+ 1.0
317
+
318
+ Compute the softmax transformation along the first axis (i.e., the
319
+ columns).
320
+
321
+ >>> m = softmax(x, axis=0)
322
+
323
+ >>> m
324
+ array([[ 2.11942e-01, 1.01300e-05, 2.75394e-06, 3.33333e-01],
325
+ [ 2.11942e-01, 2.26030e-06, 2.47262e-03, 3.33333e-01],
326
+ [ 5.76117e-01, 9.99988e-01, 9.97525e-01, 3.33333e-01]])
327
+
328
+ >>> m.sum(axis=0)
329
+ array([ 1., 1., 1., 1.])
330
+
331
+ Compute the softmax transformation along the second axis (i.e., the rows).
332
+
333
+ >>> m = softmax(x, axis=1)
334
+ >>> m
335
+ array([[ 1.05877e-01, 6.42177e-02, 4.75736e-02, 7.82332e-01],
336
+ [ 2.42746e-03, 3.28521e-04, 9.79307e-01, 1.79366e-02],
337
+ [ 1.22094e-05, 2.68929e-01, 7.31025e-01, 3.31885e-05]])
338
+
339
+ >>> m.sum(axis=1)
340
+ array([ 1., 1., 1.])
341
+
342
+ """
343
+ xp = array_namespace(x)
344
+ x = xp.asarray(x)
345
+ x_max = xp.max(x, axis=axis, keepdims=True)
346
+ exp_x_shifted = xp.exp(x - x_max)
347
+ return exp_x_shifted / xp.sum(exp_x_shifted, axis=axis, keepdims=True)
348
+
349
+
350
+ @xp_capabilities()
351
+ def log_softmax(x, axis=None):
352
+ r"""Compute the logarithm of the softmax function.
353
+
354
+ In principle::
355
+
356
+ log_softmax(x) = log(softmax(x))
357
+
358
+ but using a more accurate implementation.
359
+
360
+ Parameters
361
+ ----------
362
+ x : array_like
363
+ Input array.
364
+ axis : int or tuple of ints, optional
365
+ Axis to compute values along. Default is None and softmax will be
366
+ computed over the entire array `x`.
367
+
368
+ Returns
369
+ -------
370
+ s : ndarray or scalar
371
+ An array with the same shape as `x`. Exponential of the result will
372
+ sum to 1 along the specified axis. If `x` is a scalar, a scalar is
373
+ returned.
374
+
375
+ Notes
376
+ -----
377
+ `log_softmax` is more accurate than ``np.log(softmax(x))`` with inputs that
378
+ make `softmax` saturate (see examples below).
379
+
380
+ .. versionadded:: 1.5.0
381
+
382
+ Examples
383
+ --------
384
+ >>> import numpy as np
385
+ >>> from scipy.special import log_softmax
386
+ >>> from scipy.special import softmax
387
+ >>> np.set_printoptions(precision=5)
388
+
389
+ >>> x = np.array([1000.0, 1.0])
390
+
391
+ >>> y = log_softmax(x)
392
+ >>> y
393
+ array([ 0., -999.])
394
+
395
+ >>> with np.errstate(divide='ignore'):
396
+ ... y = np.log(softmax(x))
397
+ ...
398
+ >>> y
399
+ array([ 0., -inf])
400
+
401
+ """
402
+ xp = array_namespace(x)
403
+ x = xp.asarray(x)
404
+
405
+ x_max = xp.max(x, axis=axis, keepdims=True)
406
+
407
+ if x_max.ndim > 0:
408
+ x_max = xpx.at(x_max, ~xp.isfinite(x_max)).set(0)
409
+ elif not xp.isfinite(x_max):
410
+ x_max = 0
411
+
412
+ tmp = x - x_max
413
+ exp_tmp = xp.exp(tmp)
414
+
415
+ # suppress warnings about log of zero
416
+ with np.errstate(divide='ignore'):
417
+ s = xp.sum(exp_tmp, axis=axis, keepdims=True)
418
+ out = xp.log(s)
419
+
420
+ return tmp - out
v12x/pylib/scipy/special/_mptestutils.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ from itertools import zip_longest
5
+
6
+ import numpy as np
7
+ from numpy.testing import assert_
8
+ import pytest
9
+
10
+ from scipy.special._testutils import assert_func_equal
11
+
12
+ try:
13
+ import mpmath
14
+ except ImportError:
15
+ pass
16
+
17
+
18
+ # ------------------------------------------------------------------------------
19
+ # Machinery for systematic tests with mpmath
20
+ # ------------------------------------------------------------------------------
21
+
22
+ class Arg:
23
+ """Generate a set of numbers on the real axis, concentrating on
24
+ 'interesting' regions and covering all orders of magnitude.
25
+
26
+ """
27
+
28
+ def __init__(self, a=-np.inf, b=np.inf, inclusive_a=True, inclusive_b=True):
29
+ if a > b:
30
+ raise ValueError("a should be less than or equal to b")
31
+ if a == -np.inf:
32
+ a = -0.5*np.finfo(float).max
33
+ if b == np.inf:
34
+ b = 0.5*np.finfo(float).max
35
+ self.a, self.b = a, b
36
+
37
+ self.inclusive_a, self.inclusive_b = inclusive_a, inclusive_b
38
+
39
+ def _positive_values(self, a, b, n):
40
+ if a < 0:
41
+ raise ValueError("a should be positive")
42
+
43
+ # Try to put half of the points into a linspace between a and
44
+ # 10 the other half in a logspace.
45
+ if n % 2 == 0:
46
+ nlogpts = n//2
47
+ nlinpts = nlogpts
48
+ else:
49
+ nlogpts = n//2
50
+ nlinpts = nlogpts + 1
51
+
52
+ if a >= 10:
53
+ # Outside of linspace range; just return a logspace.
54
+ pts = np.logspace(np.log10(a), np.log10(b), n)
55
+ elif a > 0 and b < 10:
56
+ # Outside of logspace range; just return a linspace
57
+ pts = np.linspace(a, b, n)
58
+ elif a > 0:
59
+ # Linspace between a and 10 and a logspace between 10 and
60
+ # b.
61
+ linpts = np.linspace(a, 10, nlinpts, endpoint=False)
62
+ logpts = np.logspace(1, np.log10(b), nlogpts)
63
+ pts = np.hstack((linpts, logpts))
64
+ elif a == 0 and b <= 10:
65
+ # Linspace between 0 and b and a logspace between 0 and
66
+ # the smallest positive point of the linspace
67
+ linpts = np.linspace(0, b, nlinpts)
68
+ if linpts.size > 1:
69
+ right = np.log10(linpts[1])
70
+ else:
71
+ right = -30
72
+ logpts = np.logspace(-30, right, nlogpts, endpoint=False)
73
+ pts = np.hstack((logpts, linpts))
74
+ else:
75
+ # Linspace between 0 and 10, logspace between 0 and the
76
+ # smallest positive point of the linspace, and a logspace
77
+ # between 10 and b.
78
+ if nlogpts % 2 == 0:
79
+ nlogpts1 = nlogpts//2
80
+ nlogpts2 = nlogpts1
81
+ else:
82
+ nlogpts1 = nlogpts//2
83
+ nlogpts2 = nlogpts1 + 1
84
+ linpts = np.linspace(0, 10, nlinpts, endpoint=False)
85
+ if linpts.size > 1:
86
+ right = np.log10(linpts[1])
87
+ else:
88
+ right = -30
89
+ logpts1 = np.logspace(-30, right, nlogpts1, endpoint=False)
90
+ logpts2 = np.logspace(1, np.log10(b), nlogpts2)
91
+ pts = np.hstack((logpts1, linpts, logpts2))
92
+
93
+ return np.sort(pts)
94
+
95
+ def values(self, n):
96
+ """Return an array containing n numbers."""
97
+ a, b = self.a, self.b
98
+ if a == b:
99
+ return np.zeros(n)
100
+
101
+ if not self.inclusive_a:
102
+ n += 1
103
+ if not self.inclusive_b:
104
+ n += 1
105
+
106
+ if n % 2 == 0:
107
+ n1 = n//2
108
+ n2 = n1
109
+ else:
110
+ n1 = n//2
111
+ n2 = n1 + 1
112
+
113
+ if a >= 0:
114
+ pospts = self._positive_values(a, b, n)
115
+ negpts = []
116
+ elif b <= 0:
117
+ pospts = []
118
+ negpts = -self._positive_values(-b, -a, n)
119
+ else:
120
+ pospts = self._positive_values(0, b, n1)
121
+ negpts = -self._positive_values(0, -a, n2 + 1)
122
+ # Don't want to get zero twice
123
+ negpts = negpts[1:]
124
+ pts = np.hstack((negpts[::-1], pospts))
125
+
126
+ if not self.inclusive_a:
127
+ pts = pts[1:]
128
+ if not self.inclusive_b:
129
+ pts = pts[:-1]
130
+ return pts
131
+
132
+
133
+ class FixedArg:
134
+ def __init__(self, values):
135
+ self._values = np.asarray(values)
136
+
137
+ def values(self, n):
138
+ return self._values
139
+
140
+
141
+ class ComplexArg:
142
+ def __init__(self, a=complex(-np.inf, -np.inf), b=complex(np.inf, np.inf)):
143
+ self.real = Arg(a.real, b.real)
144
+ self.imag = Arg(a.imag, b.imag)
145
+
146
+ def values(self, n):
147
+ m = int(np.floor(np.sqrt(n)))
148
+ x = self.real.values(m)
149
+ y = self.imag.values(m + 1)
150
+ return (x[:,None] + 1j*y[None,:]).ravel()
151
+
152
+
153
+ class IntArg:
154
+ def __init__(self, a=-1000, b=1000):
155
+ self.a = a
156
+ self.b = b
157
+
158
+ def values(self, n):
159
+ v1 = Arg(self.a, self.b).values(max(1 + n//2, n-5)).astype(int)
160
+ v2 = np.arange(-5, 5)
161
+ v = np.unique(np.r_[v1, v2])
162
+ v = v[(v >= self.a) & (v < self.b)]
163
+ return v
164
+
165
+
166
+ def get_args(argspec, n):
167
+ if isinstance(argspec, np.ndarray):
168
+ args = argspec.copy()
169
+ else:
170
+ nargs = len(argspec)
171
+ ms = np.asarray(
172
+ [1.5 if isinstance(spec, ComplexArg) else 1.0 for spec in argspec]
173
+ )
174
+ ms = (n**(ms/sum(ms))).astype(int) + 1
175
+
176
+ args = [spec.values(m) for spec, m in zip(argspec, ms)]
177
+ args = np.array(np.broadcast_arrays(*np.ix_(*args))).reshape(nargs, -1).T
178
+
179
+ return args
180
+
181
+
182
+ class MpmathData:
183
+ def __init__(self, scipy_func, mpmath_func, arg_spec, name=None,
184
+ dps=None, prec=None, n=None, rtol=1e-7, atol=1e-300,
185
+ ignore_inf_sign=False, distinguish_nan_and_inf=True,
186
+ nan_ok=True, param_filter=None):
187
+
188
+ # mpmath tests are really slow (see gh-6989). Use a small number of
189
+ # points by default, increase back to 5000 (old default) if XSLOW is
190
+ # set
191
+ if n is None:
192
+ try:
193
+ is_xslow = int(os.environ.get('SCIPY_XSLOW', '0'))
194
+ except ValueError:
195
+ is_xslow = False
196
+
197
+ n = 5000 if is_xslow else 500
198
+
199
+ self.scipy_func = scipy_func
200
+ self.mpmath_func = mpmath_func
201
+ self.arg_spec = arg_spec
202
+ self.dps = dps
203
+ self.prec = prec
204
+ self.n = n
205
+ self.rtol = rtol
206
+ self.atol = atol
207
+ self.ignore_inf_sign = ignore_inf_sign
208
+ self.nan_ok = nan_ok
209
+ if isinstance(self.arg_spec, np.ndarray):
210
+ self.is_complex = np.issubdtype(self.arg_spec.dtype, np.complexfloating)
211
+ else:
212
+ self.is_complex = any(
213
+ [isinstance(arg, ComplexArg) for arg in self.arg_spec]
214
+ )
215
+ self.ignore_inf_sign = ignore_inf_sign
216
+ self.distinguish_nan_and_inf = distinguish_nan_and_inf
217
+ if not name or name == '<lambda>':
218
+ name = getattr(scipy_func, '__name__', None)
219
+ if not name or name == '<lambda>':
220
+ name = getattr(mpmath_func, '__name__', None)
221
+ self.name = name
222
+ self.param_filter = param_filter
223
+
224
+ def check(self):
225
+ np.random.seed(1234)
226
+
227
+ # Generate values for the arguments
228
+ argarr = get_args(self.arg_spec, self.n)
229
+
230
+ # Check
231
+ old_dps, old_prec = mpmath.mp.dps, mpmath.mp.prec
232
+ try:
233
+ if self.dps is not None:
234
+ dps_list = [self.dps]
235
+ else:
236
+ dps_list = [20]
237
+ if self.prec is not None:
238
+ mpmath.mp.prec = self.prec
239
+
240
+ # Proper casting of mpmath input and output types. Using
241
+ # native mpmath types as inputs gives improved precision
242
+ # in some cases.
243
+ if np.issubdtype(argarr.dtype, np.complexfloating):
244
+ pytype = mpc2complex
245
+
246
+ def mptype(x):
247
+ return mpmath.mpc(complex(x))
248
+ else:
249
+ def mptype(x):
250
+ return mpmath.mpf(float(x))
251
+
252
+ def pytype(x):
253
+ if abs(x.imag) > 1e-16*(1 + abs(x.real)):
254
+ return np.nan
255
+ else:
256
+ return mpf2float(x.real)
257
+
258
+ # Try out different dps until one (or none) works
259
+ for j, dps in enumerate(dps_list):
260
+ mpmath.mp.dps = dps
261
+
262
+ try:
263
+ assert_func_equal(
264
+ self.scipy_func,
265
+ lambda *a: pytype(self.mpmath_func(*map(mptype, a))),
266
+ argarr,
267
+ vectorized=False,
268
+ rtol=self.rtol,
269
+ atol=self.atol,
270
+ ignore_inf_sign=self.ignore_inf_sign,
271
+ distinguish_nan_and_inf=self.distinguish_nan_and_inf,
272
+ nan_ok=self.nan_ok,
273
+ param_filter=self.param_filter
274
+ )
275
+ break
276
+ except AssertionError:
277
+ if j >= len(dps_list)-1:
278
+ # reraise the Exception
279
+ tp, value, tb = sys.exc_info()
280
+ if value.__traceback__ is not tb:
281
+ raise value.with_traceback(tb)
282
+ raise value
283
+ finally:
284
+ mpmath.mp.dps, mpmath.mp.prec = old_dps, old_prec
285
+
286
+ def __repr__(self):
287
+ if self.is_complex:
288
+ return f"<MpmathData: {self.name} (complex)>"
289
+ else:
290
+ return f"<MpmathData: {self.name}>"
291
+
292
+
293
+ def assert_mpmath_equal(*a, **kw):
294
+ d = MpmathData(*a, **kw)
295
+ d.check()
296
+
297
+
298
+ def nonfunctional_tooslow(func):
299
+ return pytest.mark.skip(
300
+ reason=" Test not yet functional (too slow), needs more work."
301
+ )(func)
302
+
303
+
304
+ # ------------------------------------------------------------------------------
305
+ # Tools for dealing with mpmath quirks
306
+ # ------------------------------------------------------------------------------
307
+
308
+ def mpf2float(x):
309
+ """
310
+ Convert an mpf to the nearest floating point number. Just using
311
+ float directly doesn't work because of results like this:
312
+
313
+ with mp.workdps(50):
314
+ float(mpf("0.99999999999999999")) = 0.9999999999999999
315
+
316
+ """
317
+ return float(mpmath.nstr(x, 17, min_fixed=0, max_fixed=0))
318
+
319
+
320
+ def mpc2complex(x):
321
+ return complex(mpf2float(x.real), mpf2float(x.imag))
322
+
323
+
324
+ def trace_args(func):
325
+ def tofloat(x):
326
+ if isinstance(x, mpmath.mpc):
327
+ return complex(x)
328
+ else:
329
+ return float(x)
330
+
331
+ def wrap(*a, **kw):
332
+ sys.stderr.write(f"{tuple(map(tofloat, a))!r}: ")
333
+ sys.stderr.flush()
334
+ try:
335
+ r = func(*a, **kw)
336
+ sys.stderr.write(f"-> {r!r}")
337
+ finally:
338
+ sys.stderr.write("\n")
339
+ sys.stderr.flush()
340
+ return r
341
+ return wrap
342
+
343
+
344
+ try:
345
+ import signal
346
+ POSIX = ('setitimer' in dir(signal))
347
+ except ImportError:
348
+ POSIX = False
349
+
350
+
351
+ class TimeoutError(Exception):
352
+ pass
353
+
354
+
355
+ def time_limited(timeout=0.5, return_val=np.nan, use_sigalrm=True):
356
+ """
357
+ Decorator for setting a timeout for pure-Python functions.
358
+
359
+ If the function does not return within `timeout` seconds, the
360
+ value `return_val` is returned instead.
361
+
362
+ On POSIX this uses SIGALRM by default. On non-POSIX, settrace is
363
+ used. Do not use this with threads: the SIGALRM implementation
364
+ does probably not work well. The settrace implementation only
365
+ traces the current thread.
366
+
367
+ The settrace implementation slows down execution speed. Slowdown
368
+ by a factor around 10 is probably typical.
369
+ """
370
+ if POSIX and use_sigalrm:
371
+ def sigalrm_handler(signum, frame):
372
+ raise TimeoutError()
373
+
374
+ def deco(func):
375
+ def wrap(*a, **kw):
376
+ old_handler = signal.signal(signal.SIGALRM, sigalrm_handler)
377
+ signal.setitimer(signal.ITIMER_REAL, timeout)
378
+ try:
379
+ return func(*a, **kw)
380
+ except TimeoutError:
381
+ return return_val
382
+ finally:
383
+ signal.setitimer(signal.ITIMER_REAL, 0)
384
+ signal.signal(signal.SIGALRM, old_handler)
385
+ return wrap
386
+ else:
387
+ def deco(func):
388
+ def wrap(*a, **kw):
389
+ start_time = time.time()
390
+
391
+ def trace(frame, event, arg):
392
+ if time.time() - start_time > timeout:
393
+ raise TimeoutError()
394
+ return trace
395
+ sys.settrace(trace)
396
+ try:
397
+ return func(*a, **kw)
398
+ except TimeoutError:
399
+ sys.settrace(None)
400
+ return return_val
401
+ finally:
402
+ sys.settrace(None)
403
+ return wrap
404
+ return deco
405
+
406
+
407
+ def exception_to_nan(func):
408
+ """Decorate function to return nan if it raises an exception"""
409
+ def wrap(*a, **kw):
410
+ try:
411
+ return func(*a, **kw)
412
+ except Exception:
413
+ return np.nan
414
+ return wrap
415
+
416
+
417
+ def inf_to_nan(func):
418
+ """Decorate function to return nan if it returns inf"""
419
+ def wrap(*a, **kw):
420
+ v = func(*a, **kw)
421
+ if not np.isfinite(v):
422
+ return np.nan
423
+ return v
424
+ return wrap
425
+
426
+
427
+ def mp_assert_allclose(res, std, atol=0, rtol=1e-17):
428
+ """
429
+ Compare lists of mpmath.mpf's or mpmath.mpc's directly so that it
430
+ can be done to higher precision than double.
431
+ """
432
+ failures = []
433
+ for k, (resval, stdval) in enumerate(zip_longest(res, std)):
434
+ if resval is None or stdval is None:
435
+ raise ValueError('Lengths of inputs res and std are not equal.')
436
+ if mpmath.fabs(resval - stdval) > atol + rtol*mpmath.fabs(stdval):
437
+ failures.append((k, resval, stdval))
438
+
439
+ nfail = len(failures)
440
+ if nfail > 0:
441
+ ndigits = int(abs(np.log10(rtol)))
442
+ msg = [""]
443
+ msg.append(f"Bad results ({nfail} out of {k + 1}) for the following points:")
444
+ for k, resval, stdval in failures:
445
+ resrep = mpmath.nstr(resval, ndigits, min_fixed=0, max_fixed=0)
446
+ stdrep = mpmath.nstr(stdval, ndigits, min_fixed=0, max_fixed=0)
447
+ if stdval == 0:
448
+ rdiff = "inf"
449
+ else:
450
+ rdiff = mpmath.fabs((resval - stdval)/stdval)
451
+ rdiff = mpmath.nstr(rdiff, 3)
452
+ msg.append(f"{k}: {resrep} != {stdrep} (rdiff {rdiff})")
453
+ assert_(False, "\n".join(msg))
v12x/pylib/scipy/special/_multiufuncs.py ADDED
@@ -0,0 +1,744 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import collections
2
+ import numbers
3
+ import numpy as np
4
+
5
+ from ._input_validation import _nonneg_int_or_fail
6
+
7
+ from ._special_ufuncs import (legendre_p, assoc_legendre_p,
8
+ sph_legendre_p, sph_harm_y)
9
+ from ._gufuncs import (legendre_p_all, assoc_legendre_p_all,
10
+ sph_legendre_p_all, sph_harm_y_all)
11
+
12
+ __all__ = [
13
+ "assoc_legendre_p",
14
+ "assoc_legendre_p_all",
15
+ "legendre_p",
16
+ "legendre_p_all",
17
+ "sph_harm_y",
18
+ "sph_harm_y_all",
19
+ "sph_legendre_p",
20
+ "sph_legendre_p_all",
21
+ ]
22
+
23
+
24
+ class MultiUFunc:
25
+ def __init__(self, ufunc_or_ufuncs, name=None, doc=None, *,
26
+ force_complex_output=False, **default_kwargs):
27
+ if not isinstance(ufunc_or_ufuncs, np.ufunc):
28
+ if isinstance(ufunc_or_ufuncs, collections.abc.Mapping):
29
+ ufuncs_iter = ufunc_or_ufuncs.values()
30
+ elif isinstance(ufunc_or_ufuncs, collections.abc.Iterable):
31
+ ufuncs_iter = ufunc_or_ufuncs
32
+ else:
33
+ raise ValueError("ufunc_or_ufuncs should be a ufunc or a"
34
+ " ufunc collection")
35
+
36
+ # Perform input validation to ensure all ufuncs in ufuncs are
37
+ # actually ufuncs and all take the same input types.
38
+ seen_input_types = set()
39
+ for ufunc in ufuncs_iter:
40
+ if not isinstance(ufunc, np.ufunc):
41
+ raise ValueError("All ufuncs must have type `numpy.ufunc`."
42
+ f" Received {ufunc_or_ufuncs}")
43
+ seen_input_types.add(frozenset(x.split("->")[0] for x in ufunc.types))
44
+ if len(seen_input_types) > 1:
45
+ raise ValueError("All ufuncs must take the same input types.")
46
+
47
+ self.__name__ = name
48
+ self._ufunc_or_ufuncs = ufunc_or_ufuncs
49
+ self.__doc = doc
50
+ self.__force_complex_output = force_complex_output
51
+ self._default_kwargs = default_kwargs
52
+ self._resolve_out_shapes = None
53
+ self._finalize_out = None
54
+ self._key = None
55
+ self._ufunc_default_args = lambda *args, **kwargs: ()
56
+ self._ufunc_default_kwargs = lambda *args, **kwargs: {}
57
+ self.__text_signature__ = doc.split("\n\n")[0] if doc else None
58
+
59
+ @property
60
+ def __doc__(self): # pyrefly:ignore[bad-override]
61
+ return self.__doc
62
+
63
+ def _override_key(self, func):
64
+ """Set `key` method by decorating a function.
65
+ """
66
+ self._key = func
67
+
68
+ def _override_ufunc_default_args(self, func):
69
+ self._ufunc_default_args = func
70
+
71
+ def _override_ufunc_default_kwargs(self, func):
72
+ self._ufunc_default_kwargs = func
73
+
74
+ def _override_resolve_out_shapes(self, func):
75
+ """Set `resolve_out_shapes` method by decorating a function."""
76
+ if func.__doc__ is None:
77
+ func.__doc__ = \
78
+ """Resolve to output shapes based on relevant inputs."""
79
+ func.__name__ = "resolve_out_shapes"
80
+ self._resolve_out_shapes = func
81
+
82
+ def _override_finalize_out(self, func):
83
+ self._finalize_out = func
84
+
85
+ def _resolve_ufunc(self, **kwargs):
86
+ """Resolve to a ufunc based on keyword arguments."""
87
+
88
+ if isinstance(self._ufunc_or_ufuncs, np.ufunc):
89
+ return self._ufunc_or_ufuncs
90
+
91
+ ufunc_key = self._key(**kwargs)
92
+ return self._ufunc_or_ufuncs[ufunc_key]
93
+
94
+ def __call__(self, *args, **kwargs):
95
+ kwargs = self._default_kwargs | kwargs
96
+
97
+ args += self._ufunc_default_args(**kwargs)
98
+
99
+ ufunc = self._resolve_ufunc(**kwargs)
100
+
101
+ # array arguments to be passed to the ufunc
102
+ ufunc_args = [np.asarray(arg) for arg in args[-ufunc.nin:]]
103
+
104
+ ufunc_kwargs = self._ufunc_default_kwargs(**kwargs)
105
+
106
+ if (self._resolve_out_shapes is not None):
107
+ ufunc_arg_shapes = tuple(np.shape(ufunc_arg) for ufunc_arg in ufunc_args)
108
+ ufunc_out_shapes = self._resolve_out_shapes(*args[:-ufunc.nin],
109
+ *ufunc_arg_shapes, ufunc.nout,
110
+ **kwargs)
111
+
112
+ ufunc_arg_dtypes = tuple(ufunc_arg.dtype if hasattr(ufunc_arg, 'dtype')
113
+ else np.dtype(type(ufunc_arg))
114
+ for ufunc_arg in ufunc_args)
115
+
116
+ if hasattr(ufunc, 'resolve_dtypes'):
117
+ ufunc_dtypes = ufunc_arg_dtypes + ufunc.nout * (None,)
118
+ ufunc_dtypes = ufunc.resolve_dtypes(ufunc_dtypes)
119
+ ufunc_out_dtypes = ufunc_dtypes[-ufunc.nout:]
120
+ else:
121
+ ufunc_out_dtype = np.result_type(*ufunc_arg_dtypes)
122
+ if (not np.issubdtype(ufunc_out_dtype, np.inexact)):
123
+ ufunc_out_dtype = np.float64
124
+
125
+ ufunc_out_dtypes = ufunc.nout * (ufunc_out_dtype,)
126
+
127
+ if self.__force_complex_output:
128
+ ufunc_out_dtypes = tuple(np.result_type(1j, ufunc_out_dtype)
129
+ for ufunc_out_dtype in ufunc_out_dtypes)
130
+
131
+ out = tuple(np.empty(ufunc_out_shape, dtype=ufunc_out_dtype)
132
+ for ufunc_out_shape, ufunc_out_dtype
133
+ in zip(ufunc_out_shapes, ufunc_out_dtypes))
134
+
135
+ ufunc_kwargs['out'] = out
136
+
137
+ out = ufunc(*ufunc_args, **ufunc_kwargs)
138
+ if (self._finalize_out is not None):
139
+ out = self._finalize_out(out)
140
+
141
+ return out
142
+
143
+
144
+ sph_legendre_p = MultiUFunc(
145
+ sph_legendre_p,
146
+ "sph_legendre_p",
147
+ r"""sph_legendre_p(n, m, theta, *, diff_n=0)
148
+
149
+ Spherical Legendre polynomial of the first kind.
150
+
151
+ Parameters
152
+ ----------
153
+ n : array_like of ints
154
+ Degree of the spherical Legendre polynomial. Must have ``n >= 0``.
155
+ m : array_like of ints
156
+ Order of the spherical Legendre polynomial.
157
+ theta : array_like
158
+ Input value.
159
+ diff_n : int, optional
160
+ A non-negative integer. Compute and return all derivatives up
161
+ to order `diff_n`. Default is 0.
162
+
163
+ Returns
164
+ -------
165
+ ndarray or tuple of ndarray
166
+ Spherical Legendre polynomial with ``diff_n`` derivatives.
167
+
168
+ Notes
169
+ -----
170
+ The spherical counterpart of an (unnormalized) associated Legendre polynomial has
171
+ the additional factor
172
+
173
+ .. math::
174
+
175
+ \sqrt{\frac{(2 n + 1) (n - m)!}{4 \pi (n + m)!}}
176
+
177
+ It is the same as the spherical harmonic :math:`Y_{n}^{m}(\theta, \phi)`
178
+ with :math:`\phi = 0`.
179
+ """, diff_n=0
180
+ )
181
+
182
+
183
+ @sph_legendre_p._override_key
184
+ def _(diff_n):
185
+ diff_n = _nonneg_int_or_fail(diff_n, "diff_n", strict=False)
186
+ if not 0 <= diff_n <= 2:
187
+ raise ValueError(
188
+ "diff_n is currently only implemented for orders 0, 1, and 2,"
189
+ f" received: {diff_n}."
190
+ )
191
+ return diff_n
192
+
193
+
194
+ @sph_legendre_p._override_finalize_out
195
+ def _(out):
196
+ return np.moveaxis(out, -1, 0)
197
+
198
+
199
+ sph_legendre_p_all = MultiUFunc(
200
+ sph_legendre_p_all,
201
+ "sph_legendre_p_all",
202
+ """sph_legendre_p_all(n, m, theta, *, diff_n=0)
203
+
204
+ All spherical Legendre polynomials of the first kind up to the
205
+ specified degree `n`, order `m`, and all derivatives up
206
+ to order `diff_n`.
207
+
208
+ Parameters
209
+ ----------
210
+ n : int
211
+ Degree of the spherical Legendre polynomials. Must have ``n >= 0``.
212
+ m : int
213
+ Order of the spherical Legendre polynomials.
214
+ theta : array_like
215
+ Input value.
216
+ diff_n : int, optional
217
+ A non-negative integer. Compute and return all derivatives up
218
+ to order `diff_n`. Default is 0.
219
+
220
+ Returns
221
+ -------
222
+ ndarray
223
+ Output shape is ``(diff_n + 1, n + 1, 2 * m + 1, ...)``. The entry at
224
+ ``(i, j, k)`` corresponds to the ``i``-th derivative, degree ``j``, and
225
+ order ``k`` for all ``0 <= i <= diff_n``, ``0 <= j <= n``, and
226
+ ``-m <= k <= m``.
227
+
228
+ See Also
229
+ --------
230
+ sph_legendre_p
231
+ """, diff_n=0
232
+ )
233
+
234
+
235
+ @sph_legendre_p_all._override_key
236
+ def _(diff_n):
237
+ diff_n = _nonneg_int_or_fail(diff_n, "diff_n", strict=False)
238
+ if not 0 <= diff_n <= 2:
239
+ raise ValueError(
240
+ "diff_n is currently only implemented for orders 0, 1, and 2,"
241
+ f" received: {diff_n}."
242
+ )
243
+ return diff_n
244
+
245
+
246
+ @sph_legendre_p_all._override_ufunc_default_kwargs
247
+ def _(diff_n):
248
+ return {'axes': [()] + [(0, 1, -1)]}
249
+
250
+
251
+ @sph_legendre_p_all._override_resolve_out_shapes
252
+ def _(n, m, theta_shape, nout, diff_n):
253
+ if not isinstance(n, numbers.Integral) or (n < 0):
254
+ raise ValueError("n must be a non-negative integer.")
255
+
256
+ return ((n + 1, 2 * abs(m) + 1) + theta_shape + (diff_n + 1,),)
257
+
258
+
259
+ @sph_legendre_p_all._override_finalize_out
260
+ def _(out):
261
+ return np.moveaxis(out, -1, 0)
262
+
263
+
264
+ assoc_legendre_p = MultiUFunc(
265
+ assoc_legendre_p,
266
+ "assoc_legendre_p",
267
+ r"""assoc_legendre_p(n, m, z, *, branch_cut=2, norm=False, diff_n=0)
268
+
269
+ Associated Legendre polynomial of the first kind.
270
+
271
+ Defined as
272
+
273
+ .. math::
274
+
275
+ P_n^m(z) = (-1)^m (1 - z^2)^{m/2}
276
+ \frac{d^m}{dz^m} P_n(z)
277
+
278
+ where :math:`P_n` is the Legendre polynomial.
279
+
280
+ This definition includes the Condon-Shortley phase :math:`(-1)^m`.
281
+
282
+ Parameters
283
+ ----------
284
+ n : array_like of ints
285
+ Degree of the associated Legendre polynomial. Must have ``n >= 0``.
286
+ m : array_like of ints
287
+ Order of the associated Legendre polynomial.
288
+ z : array_like
289
+ Input value.
290
+ branch_cut : array_like of ints, optional
291
+ Selects branch cut. Must be 2 (default) or 3.
292
+ 2: cut on the real axis ``|z| > 1``
293
+ 3: cut on the real axis ``-1 < z < 1``
294
+ norm : bool, optional
295
+ If ``True``, compute the normalized associated Legendre polynomial.
296
+ Default is ``False``.
297
+ diff_n : int, optional
298
+ A non-negative integer. Compute and return all derivatives up
299
+ to order ``diff_n``. Default is 0.
300
+
301
+ Returns
302
+ -------
303
+ ndarray or tuple of ndarray
304
+ Associated Legendre polynomial with ``diff_n`` derivatives.
305
+
306
+ Notes
307
+ -----
308
+ The normalized counterpart of an (unnormalized) associated Legendre
309
+ polynomial has the additional factor
310
+
311
+ .. math::
312
+
313
+ \sqrt{\frac{(2 n + 1) (n - m)!}{2 (n + m)!}}
314
+
315
+ Examples
316
+ --------
317
+ Evaluate :math:`P_2^1(z)` on :math:`[-1, 1]`:
318
+
319
+ >>> import numpy as np
320
+ >>> from scipy.special import assoc_legendre_p
321
+ >>> z = np.linspace(-1, 1, 11)
322
+ >>> expected = -3 * z * np.sqrt(1 - z**2)
323
+ >>> np.allclose(assoc_legendre_p(2, 1, z), expected)
324
+ True
325
+
326
+ Compute the normalized associated Legendre polynomial:
327
+
328
+ >>> scale = np.sqrt(5/12)
329
+ >>> np.allclose(assoc_legendre_p(2, 1, z, norm=True), scale * expected)
330
+ True
331
+ """, branch_cut=2, norm=False, diff_n=0
332
+ )
333
+
334
+
335
+ @assoc_legendre_p._override_key
336
+ def _(branch_cut, norm, diff_n):
337
+ diff_n = _nonneg_int_or_fail(diff_n, "diff_n", strict=False)
338
+ if not 0 <= diff_n <= 2:
339
+ raise ValueError(
340
+ "diff_n is currently only implemented for orders 0, 1, and 2,"
341
+ f" received: {diff_n}."
342
+ )
343
+ return norm, diff_n
344
+
345
+
346
+ @assoc_legendre_p._override_ufunc_default_args
347
+ def _(branch_cut, norm, diff_n):
348
+ return branch_cut,
349
+
350
+
351
+ @assoc_legendre_p._override_finalize_out
352
+ def _(out):
353
+ return np.moveaxis(out, -1, 0)
354
+
355
+
356
+ assoc_legendre_p_all = MultiUFunc(
357
+ assoc_legendre_p_all,
358
+ "assoc_legendre_p_all",
359
+ """assoc_legendre_p_all(n, m, z, *, branch_cut=2, norm=False, diff_n=0)
360
+
361
+ All associated Legendre polynomials of the first kind up to the
362
+ specified degree `n`, order `m`, and all derivatives up
363
+ to order `diff_n`.
364
+
365
+ Parameters
366
+ ----------
367
+ n : int
368
+ Degree of the associated Legendre polynomials. Must have ``n >= 0``.
369
+ m : int
370
+ Order of the associated Legendre polynomials.
371
+ z : array_like
372
+ Input value.
373
+ branch_cut : array_like of ints, optional
374
+ Selects branch cut. Must be 2 (default) or 3.
375
+ 2: cut on the real axis ``|z| > 1``
376
+ 3: cut on the real axis ``-1 < z < 1``
377
+ norm : bool, optional
378
+ If ``True``, compute the normalized associated Legendre polynomials.
379
+ Default is ``False``.
380
+ diff_n : int, optional
381
+ A non-negative integer. Compute and return all derivatives up
382
+ to order ``diff_n``. Default is 0.
383
+
384
+ Returns
385
+ -------
386
+ ndarray
387
+ Output shape is ``(diff_n + 1, n + 1, 2 * m + 1, ...)``. The entry at
388
+ ``(i, j, k)`` corresponds to the ``i``-th derivative, degree ``j``, and
389
+ order ``k`` for all ``0 <= i <= diff_n``, ``0 <= j <= n``, and
390
+ ``-m <= k <= m``.
391
+
392
+ See Also
393
+ --------
394
+ assoc_legendre_p
395
+ """, branch_cut=2, norm=False, diff_n=0
396
+ )
397
+
398
+
399
+ @assoc_legendre_p_all._override_key
400
+ def _(branch_cut, norm, diff_n):
401
+ if not ((isinstance(diff_n, numbers.Integral))
402
+ and diff_n >= 0):
403
+ raise ValueError(
404
+ f"diff_n must be a non-negative integer, received: {diff_n}."
405
+ )
406
+ if not 0 <= diff_n <= 2:
407
+ raise ValueError(
408
+ "diff_n is currently only implemented for orders 0, 1, and 2,"
409
+ f" received: {diff_n}."
410
+ )
411
+ return norm, diff_n
412
+
413
+
414
+ @assoc_legendre_p_all._override_ufunc_default_args
415
+ def _(branch_cut, norm, diff_n):
416
+ return branch_cut,
417
+
418
+
419
+ @assoc_legendre_p_all._override_ufunc_default_kwargs
420
+ def _(branch_cut, norm, diff_n):
421
+ return {'axes': [(), ()] + [(0, 1, -1)]}
422
+
423
+
424
+ @assoc_legendre_p_all._override_resolve_out_shapes
425
+ def _(n, m, z_shape, branch_cut_shape, nout, **kwargs):
426
+ diff_n = kwargs['diff_n']
427
+
428
+ if not isinstance(n, numbers.Integral) or (n < 0):
429
+ raise ValueError("n must be a non-negative integer.")
430
+ if not isinstance(m, numbers.Integral) or (m < 0):
431
+ raise ValueError("m must be a non-negative integer.")
432
+
433
+ return ((n + 1, 2 * abs(m) + 1) +
434
+ np.broadcast_shapes(z_shape, branch_cut_shape) + (diff_n + 1,),)
435
+
436
+
437
+ @assoc_legendre_p_all._override_finalize_out
438
+ def _(out):
439
+ return np.moveaxis(out, -1, 0)
440
+
441
+
442
+ legendre_p = MultiUFunc(
443
+ legendre_p,
444
+ "legendre_p",
445
+ """legendre_p(n, z, *, diff_n=0)
446
+
447
+ Legendre polynomial of the first kind.
448
+
449
+ Parameters
450
+ ----------
451
+ n : array_like of ints
452
+ Degree of the Legendre polynomial. Must have ``n >= 0``.
453
+ z : array_like
454
+ Input value.
455
+ diff_n : int, optional
456
+ A non-negative integer. Compute and return all derivatives up
457
+ to order ``diff_n``. Default is 0.
458
+
459
+ Returns
460
+ -------
461
+ p : ndarray or tuple of ndarray
462
+ Legendre polynomial with ``diff_n`` derivatives.
463
+
464
+ See Also
465
+ --------
466
+ legendre
467
+
468
+ References
469
+ ----------
470
+ .. [1] Zhang, Shanjie and Jin, Jianming. "Computation of Special
471
+ Functions", John Wiley and Sons, 1996.
472
+ https://people.sc.fsu.edu/~jburkardt/f77_src/special_functions/special_functions.html
473
+
474
+ Examples
475
+ --------
476
+ Evaluate the Legendre polynomial :math:`P_3` at :math:`z = 0.5`:
477
+
478
+ >>> import numpy as np
479
+ >>> from scipy.special import legendre_p
480
+ >>> np.allclose(legendre_p(3, 0.5), -0.4375)
481
+ True
482
+
483
+ Compute the value and first derivative with respect to ``z``:
484
+
485
+ >>> p, dp = legendre_p(3, 0.5, diff_n=1)
486
+ >>> np.allclose([p, dp], [-0.4375, 0.375])
487
+ True
488
+ """, diff_n=0
489
+ )
490
+
491
+
492
+ @legendre_p._override_key
493
+ def _(diff_n):
494
+ if (not isinstance(diff_n, numbers.Integral)) or (diff_n < 0):
495
+ raise ValueError(
496
+ f"diff_n must be a non-negative integer, received: {diff_n}."
497
+ )
498
+ if not 0 <= diff_n <= 2:
499
+ raise NotImplementedError(
500
+ "diff_n is currently only implemented for orders 0, 1, and 2,"
501
+ f" received: {diff_n}."
502
+ )
503
+ return diff_n
504
+
505
+
506
+ @legendre_p._override_finalize_out
507
+ def _(out):
508
+ return np.moveaxis(out, -1, 0)
509
+
510
+
511
+ legendre_p_all = MultiUFunc(
512
+ legendre_p_all,
513
+ "legendre_p_all",
514
+ """legendre_p_all(n, z, *, diff_n=0)
515
+
516
+ All Legendre polynomials of the first kind up to the specified degree
517
+ `n` and all derivatives up to order `diff_n`.
518
+
519
+ Parameters
520
+ ----------
521
+ n : int
522
+ Degree of the Legendre polynomials. Must have ``n >= 0``.
523
+ z : array_like
524
+ Input value.
525
+ diff_n : int, optional
526
+ A non-negative integer. Compute and return all derivatives up
527
+ to order ``diff_n``. Default is 0.
528
+
529
+ Returns
530
+ -------
531
+ ndarray
532
+ Output shape is ``(diff_n + 1, n + 1, ...)``. The entry at ``(i, j)``
533
+ corresponds to the ``i``-th derivative and degree ``j`` for all
534
+ ``0 <= i <= diff_n`` and ``0 <= j <= n``.
535
+
536
+ See Also
537
+ --------
538
+ legendre_p
539
+ """, diff_n=0
540
+ )
541
+
542
+
543
+ @legendre_p_all._override_key
544
+ def _(diff_n):
545
+ diff_n = _nonneg_int_or_fail(diff_n, "diff_n", strict=False)
546
+ if not 0 <= diff_n <= 2:
547
+ raise ValueError(
548
+ "diff_n is currently only implemented for orders 0, 1, and 2,"
549
+ f" received: {diff_n}."
550
+ )
551
+ return diff_n
552
+
553
+
554
+ @legendre_p_all._override_ufunc_default_kwargs
555
+ def _(diff_n):
556
+ return {'axes': [(), (0, -1)]}
557
+
558
+
559
+ @legendre_p_all._override_resolve_out_shapes
560
+ def _(n, z_shape, nout, diff_n):
561
+ n = _nonneg_int_or_fail(n, 'n', strict=False)
562
+
563
+ return nout * ((n + 1,) + z_shape + (diff_n + 1,),)
564
+
565
+
566
+ @legendre_p_all._override_finalize_out
567
+ def _(out):
568
+ return np.moveaxis(out, -1, 0)
569
+
570
+
571
+ sph_harm_y = MultiUFunc(
572
+ sph_harm_y,
573
+ "sph_harm_y",
574
+ r"""sph_harm_y(n, m, theta, phi, *, diff_n=0)
575
+
576
+ Spherical harmonics.
577
+
578
+ They are defined as
579
+
580
+ .. math::
581
+
582
+ Y_n^m(\theta,\phi) = \sqrt{\frac{2 n + 1}{4 \pi} \frac{(n - m)!}{(n + m)!}}
583
+ P_n^m(\cos(\theta)) e^{i m \phi}
584
+
585
+ where :math:`P_n^m` are the (unnormalized) associated Legendre polynomials.
586
+
587
+ Parameters
588
+ ----------
589
+ n : array_like of ints
590
+ Degree of the harmonic. Must have ``n >= 0``. This is
591
+ often denoted by ``l`` (lower case L) in descriptions of
592
+ spherical harmonics.
593
+ m : array_like of ints
594
+ Order of the harmonic.
595
+ theta : array_like
596
+ Polar (colatitudinal) coordinate; must be in ``[0, pi]``.
597
+ phi : array_like
598
+ Azimuthal (longitudinal) coordinate; must be in ``[0, 2*pi]``.
599
+ diff_n : int, optional
600
+ A non-negative integer. Compute and return all derivatives up
601
+ to order `diff_n`. Default is 0.
602
+
603
+ Returns
604
+ -------
605
+ ndarray or tuple of ndarray
606
+ Spherical harmonics with `diff_n` derivatives.
607
+
608
+ Notes
609
+ -----
610
+ There are different conventions for the meanings of the input
611
+ arguments ``theta`` and ``phi``. In SciPy ``theta`` is the
612
+ polar angle and ``phi`` is the azimuthal angle. It is common to
613
+ see the opposite convention, that is, ``theta`` as the azimuthal angle
614
+ and ``phi`` as the polar angle.
615
+
616
+ Note that SciPy's spherical harmonics include the Condon-Shortley
617
+ phase [2]_ because it is part of `sph_legendre_p`.
618
+
619
+ With SciPy's conventions, the first several spherical harmonics
620
+ are
621
+
622
+ .. math::
623
+
624
+ Y_0^0(\theta, \phi) &= \frac{1}{2} \sqrt{\frac{1}{\pi}} \\
625
+ Y_1^{-1}(\theta, \phi) &= \frac{1}{2} \sqrt{\frac{3}{2\pi}}
626
+ e^{-i\phi} \sin(\theta) \\
627
+ Y_1^0(\theta, \phi) &= \frac{1}{2} \sqrt{\frac{3}{\pi}}
628
+ \cos(\theta) \\
629
+ Y_1^1(\theta, \phi) &= -\frac{1}{2} \sqrt{\frac{3}{2\pi}}
630
+ e^{i\phi} \sin(\theta).
631
+
632
+ References
633
+ ----------
634
+ .. [1] Digital Library of Mathematical Functions, 14.30.
635
+ https://dlmf.nist.gov/14.30
636
+ .. [2] https://en.wikipedia.org/wiki/Spherical_harmonics#Condon.E2.80.93Shortley_phase
637
+ """, force_complex_output=True, diff_n=0
638
+ )
639
+
640
+
641
+ @sph_harm_y._override_key
642
+ def _(diff_n):
643
+ diff_n = _nonneg_int_or_fail(diff_n, "diff_n", strict=False)
644
+ if not 0 <= diff_n <= 2:
645
+ raise ValueError(
646
+ "diff_n is currently only implemented for orders 0, 1, and 2,"
647
+ f" received: {diff_n}."
648
+ )
649
+ return diff_n
650
+
651
+
652
+ @sph_harm_y._override_finalize_out
653
+ def _(out):
654
+ if (out.shape[-1] == 1):
655
+ return out[..., 0, 0]
656
+
657
+ if (out.shape[-1] == 2):
658
+ return out[..., 0, 0], out[..., [1, 0], [0, 1]]
659
+
660
+ if (out.shape[-1] == 3):
661
+ return (out[..., 0, 0], out[..., [1, 0], [0, 1]],
662
+ out[..., [[2, 1], [1, 0]], [[0, 1], [1, 2]]])
663
+
664
+
665
+ sph_harm_y_all = MultiUFunc(
666
+ sph_harm_y_all,
667
+ "sph_harm_y_all",
668
+ """sph_harm_y_all(n, m, theta, phi, *, diff_n=0)
669
+
670
+ All spherical harmonics up to the specified degree `n`, order `m`,
671
+ and all derivatives up to order `diff_n`.
672
+
673
+ Parameters
674
+ ----------
675
+ n : int
676
+ Degree of the harmonics. Must have ``n >= 0``. This is
677
+ often denoted by ``l`` (lower case L) in descriptions of
678
+ spherical harmonics.
679
+ m : int
680
+ Order of the harmonics.
681
+ theta : array_like
682
+ Polar (colatitudinal) coordinate; must be in ``[0, pi]``.
683
+ phi : array_like
684
+ Azimuthal (longitudinal) coordinate; must be in ``[0, 2*pi]``.
685
+ diff_n : int, optional
686
+ A non-negative integer. Compute and return all derivatives up
687
+ to order `diff_n`. Default is 0.
688
+
689
+ Returns
690
+ -------
691
+ ndarray or tuple of ndarray
692
+ Returns a tuple of length ``diff_n + 1`` (if ``diff_n > 0``). The first
693
+ entry corresponds to the spherical harmonics, the second entry
694
+ (if ``diff_n >= 1``) to the gradient, and the third entry
695
+ (if ``diff_n >= 2``) to the Hessian matrix. Each entry is an array of
696
+ shape ``(n + 1, 2 * m + 1, ...)``, where the entry at ``(i, j)``
697
+ corresponds to degree ``i`` and order ``j`` for all ``0 <= i <= n``
698
+ and ``-m <= j <= m``.
699
+
700
+ See Also
701
+ --------
702
+ sph_harm_y
703
+ """, force_complex_output=True, diff_n=0
704
+ )
705
+
706
+
707
+ @sph_harm_y_all._override_key
708
+ def _(diff_n):
709
+ diff_n = _nonneg_int_or_fail(diff_n, "diff_n", strict=False)
710
+ if not 0 <= diff_n <= 2:
711
+ raise ValueError(
712
+ "diff_n is currently only implemented for orders 2,"
713
+ f" received: {diff_n}."
714
+ )
715
+ return diff_n
716
+
717
+
718
+ @sph_harm_y_all._override_ufunc_default_kwargs
719
+ def _(diff_n):
720
+ return {'axes': [(), ()] + [(0, 1, -2, -1)]}
721
+
722
+
723
+ @sph_harm_y_all._override_resolve_out_shapes
724
+ def _(n, m, theta_shape, phi_shape, nout, **kwargs):
725
+ diff_n = kwargs['diff_n']
726
+
727
+ if not isinstance(n, numbers.Integral) or (n < 0):
728
+ raise ValueError("n must be a non-negative integer.")
729
+
730
+ return ((n + 1, 2 * abs(m) + 1) + np.broadcast_shapes(theta_shape, phi_shape) +
731
+ (diff_n + 1, diff_n + 1),)
732
+
733
+
734
+ @sph_harm_y_all._override_finalize_out
735
+ def _(out):
736
+ if (out.shape[-1] == 1):
737
+ return out[..., 0, 0]
738
+
739
+ if (out.shape[-1] == 2):
740
+ return out[..., 0, 0], out[..., [1, 0], [0, 1]]
741
+
742
+ if (out.shape[-1] == 3):
743
+ return (out[..., 0, 0], out[..., [1, 0], [0, 1]],
744
+ out[..., [[2, 1], [1, 0]], [[0, 1], [1, 2]]])
v12x/pylib/scipy/special/_orthogonal.py ADDED
@@ -0,0 +1,2856 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ A collection of functions to find the weights and abscissas for
3
+ Gaussian Quadrature.
4
+
5
+ These calculations are done by finding the eigenvalues of a
6
+ tridiagonal matrix whose entries are dependent on the coefficients
7
+ in the recursion formula for the orthogonal polynomials with the
8
+ corresponding weighting function over the interval.
9
+
10
+ Many recursion relations for orthogonal polynomials are given:
11
+
12
+ .. math::
13
+
14
+ a1n f_{n+1} (x) = (a2n + a3n x ) f_n (x) - a4n f_{n-1} (x)
15
+
16
+ The recursion relation of interest is
17
+
18
+ .. math::
19
+
20
+ P_{n+1} (x) = (x - A_n) P_n (x) - B_n P_{n-1} (x)
21
+
22
+ where :math:`P` has a different normalization than :math:`f`.
23
+
24
+ The coefficients can be found as:
25
+
26
+ .. math::
27
+
28
+ A_n = -a2n / a3n
29
+ \\qquad
30
+ B_n = ( a4n / a3n \\sqrt{h_n-1 / h_n})^2
31
+
32
+ where
33
+
34
+ .. math::
35
+
36
+ h_n = \\int_a^b w(x) f_n(x)^2
37
+
38
+ assume:
39
+
40
+ .. math::
41
+
42
+ P_0 (x) = 1
43
+ \\qquad
44
+ P_{-1} (x) == 0
45
+
46
+ For the mathematical background, see [golub.welsch-1969-mathcomp]_ and
47
+ [abramowitz.stegun-1965]_.
48
+
49
+ References
50
+ ----------
51
+ .. [golub.welsch-1969-mathcomp]
52
+ Golub, Gene H, and John H Welsch. 1969. Calculation of Gauss
53
+ Quadrature Rules. *Mathematics of Computation* 23, 221-230+s1--s10.
54
+
55
+ .. [abramowitz.stegun-1965]
56
+ Abramowitz, Milton, and Irene A Stegun. (1965) *Handbook of
57
+ Mathematical Functions: with Formulas, Graphs, and Mathematical
58
+ Tables*. Gaithersburg, MD: National Bureau of Standards.
59
+ http://www.math.sfu.ca/~cbm/aands/
60
+
61
+ .. [townsend.trogdon.olver-2014]
62
+ Townsend, A. and Trogdon, T. and Olver, S. (2014)
63
+ *Fast computation of Gauss quadrature nodes and
64
+ weights on the whole real line*. :arXiv:`1410.5286`.
65
+
66
+ .. [townsend.trogdon.olver-2015]
67
+ Townsend, A. and Trogdon, T. and Olver, S. (2015)
68
+ *Fast computation of Gauss quadrature nodes and
69
+ weights on the whole real line*.
70
+ IMA Journal of Numerical Analysis
71
+ :doi:`10.1093/imanum/drv002`.
72
+ """
73
+ #
74
+ # Author: Travis Oliphant 2000
75
+ # Updated Sep. 2003 (fixed bugs --- tested to be accurate)
76
+
77
+ # SciPy imports.
78
+ import numpy as np
79
+ from numpy import (exp, inf, pi, sqrt, floor, sin, cos, around,
80
+ hstack, arccos, arange)
81
+ from scipy.special import airy
82
+
83
+ # Local imports.
84
+ # There is no .pyi file for _specfun
85
+ from . import _specfun # type: ignore
86
+ from . import _ufuncs
87
+ _gam = _ufuncs.gamma
88
+
89
+ _polyfuns = ['legendre', 'chebyt', 'chebyu', 'chebyc', 'chebys',
90
+ 'jacobi', 'laguerre', 'genlaguerre', 'hermite',
91
+ 'hermitenorm', 'gegenbauer', 'sh_legendre', 'sh_chebyt',
92
+ 'sh_chebyu', 'sh_jacobi']
93
+
94
+ # Correspondence between new and old names of root functions
95
+ _rootfuns_map = {'roots_legendre': 'p_roots',
96
+ 'roots_chebyt': 't_roots',
97
+ 'roots_chebyu': 'u_roots',
98
+ 'roots_chebyc': 'c_roots',
99
+ 'roots_chebys': 's_roots',
100
+ 'roots_jacobi': 'j_roots',
101
+ 'roots_laguerre': 'l_roots',
102
+ 'roots_genlaguerre': 'la_roots',
103
+ 'roots_hermite': 'h_roots',
104
+ 'roots_hermitenorm': 'he_roots',
105
+ 'roots_gegenbauer': 'cg_roots',
106
+ 'roots_sh_legendre': 'ps_roots',
107
+ 'roots_sh_chebyt': 'ts_roots',
108
+ 'roots_sh_chebyu': 'us_roots',
109
+ 'roots_sh_jacobi': 'js_roots'}
110
+
111
+ __all__ = _polyfuns + list(_rootfuns_map.keys())
112
+
113
+
114
+ class orthopoly1d(np.poly1d):
115
+
116
+ def __init__(self, roots, weights=None, hn=1.0, kn=1.0, wfunc=None,
117
+ limits=None, monic=False, eval_func=None):
118
+ equiv_weights = [weights[k] / wfunc(roots[k]) for
119
+ k in range(len(roots))]
120
+ mu = sqrt(hn)
121
+ if monic:
122
+ evf = eval_func
123
+ if evf:
124
+ knn = kn
125
+ def eval_func(x):
126
+ return evf(x) / knn
127
+ mu = mu / abs(kn)
128
+ kn = 1.0
129
+
130
+ # compute coefficients from roots, then scale
131
+ poly = np.poly1d(roots, r=True)
132
+ np.poly1d.__init__(self, poly.coeffs * float(kn))
133
+
134
+ self.weights = np.array(list(zip(roots, weights, equiv_weights)))
135
+ self.weight_func = wfunc
136
+ self.limits = limits
137
+ self.normcoef = mu
138
+
139
+ # Note: eval_func will be discarded on arithmetic
140
+ self._eval_func = eval_func
141
+
142
+ def __call__(self, v):
143
+ if self._eval_func and not isinstance(v, np.poly1d):
144
+ return self._eval_func(v)
145
+ else:
146
+ return np.poly1d.__call__(self, v)
147
+
148
+ def _scale(self, p):
149
+ if p == 1.0:
150
+ return
151
+ self._coeffs *= p
152
+
153
+ evf = self._eval_func
154
+ if evf:
155
+ self._eval_func = lambda x: evf(x) * p
156
+ self.normcoef *= p
157
+
158
+
159
+ def _gen_roots_and_weights(n, mu0, an_func, bn_func, f, df, symmetrize, mu):
160
+ """[x,w] = gen_roots_and_weights(n,an_func,sqrt_bn_func,mu)
161
+
162
+ Returns the roots (x) of an nth order orthogonal polynomial,
163
+ and weights (w) to use in appropriate Gaussian quadrature with that
164
+ orthogonal polynomial.
165
+
166
+ The polynomials have the recurrence relation
167
+ P_n+1(x) = (x - A_n) P_n(x) - B_n P_n-1(x)
168
+
169
+ an_func(n) should return A_n
170
+ sqrt_bn_func(n) should return sqrt(B_n)
171
+ mu ( = h_0 ) is the integral of the weight over the orthogonal
172
+ interval
173
+ """
174
+ # lazy import to prevent to prevent linalg dependency for whole module (gh-23420)
175
+ from scipy import linalg
176
+ k = np.arange(n, dtype='d')
177
+ c = np.zeros((2, n))
178
+ c[0,1:] = bn_func(k[1:])
179
+ c[1,:] = an_func(k)
180
+ x = linalg.eigvals_banded(c, overwrite_a_band=True)
181
+
182
+ # improve roots by one application of Newton's method
183
+ y = f(n, x)
184
+ dy = df(n, x)
185
+ x -= y/dy
186
+
187
+ # fm and dy may contain very large/small values, so we
188
+ # log-normalize them to maintain precision in the product fm*dy
189
+ fm = f(n-1, x)
190
+ log_fm = np.log(np.abs(fm))
191
+ log_dy = np.log(np.abs(dy))
192
+ fm /= np.exp((log_fm.max() + log_fm.min()) / 2.)
193
+ dy /= np.exp((log_dy.max() + log_dy.min()) / 2.)
194
+ w = 1.0 / (fm * dy)
195
+
196
+ if symmetrize:
197
+ w = (w + w[::-1]) / 2
198
+ x = (x - x[::-1]) / 2
199
+
200
+ w *= mu0 / w.sum()
201
+
202
+ if mu:
203
+ return x, w, mu0
204
+ else:
205
+ return x, w
206
+
207
+ # Jacobi Polynomials 1 P^(alpha,beta)_n(x)
208
+
209
+
210
+ def roots_jacobi(n, alpha, beta, mu=False):
211
+ r"""Gauss-Jacobi quadrature.
212
+
213
+ Compute the sample points and weights for Gauss-Jacobi
214
+ quadrature. The sample points are the roots of the nth degree
215
+ Jacobi polynomial, :math:`P^{\alpha, \beta}_n(x)`. These sample
216
+ points and weights correctly integrate polynomials of degree
217
+ :math:`2n - 1` or less over the interval :math:`[-1, 1]` with
218
+ weight function :math:`w(x) = (1 - x)^{\alpha} (1 +
219
+ x)^{\beta}`. See 22.2.1 in [AS]_ for details.
220
+
221
+ Parameters
222
+ ----------
223
+ n : int
224
+ Quadrature order.
225
+ alpha : float
226
+ alpha must be > -1.
227
+ beta : float
228
+ beta must be > -1.
229
+ mu : bool, optional
230
+ If True, return the sum of the weights in addition to sample points and weights.
231
+
232
+ Returns
233
+ -------
234
+ x : ndarray
235
+ Sample points.
236
+ w : ndarray
237
+ Weights.
238
+ mu : float, optional
239
+ Sum of the weights, only returned if `mu=True`.
240
+
241
+ See Also
242
+ --------
243
+ scipy.integrate.fixed_quad
244
+
245
+ References
246
+ ----------
247
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
248
+ Handbook of Mathematical Functions with Formulas,
249
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
250
+
251
+ Examples
252
+ --------
253
+ >>> from scipy.special import roots_jacobi
254
+ >>> x, w = roots_jacobi(3, 0.5, 0.5)
255
+ >>> x
256
+ array([-0.70710678, 0. , 0.70710678])
257
+ >>> w
258
+ array([0.39269908, 0.78539816, 0.39269908])
259
+
260
+ >>> x, w, mu = roots_jacobi(3, 0.5, 0.5, mu=True)
261
+ >>> mu
262
+ 1.5707963267948966 # Sum of weights, equals pi/2 for alpha = beta = 0.5
263
+ """
264
+
265
+ m = int(n)
266
+ if n < 1 or n != m:
267
+ raise ValueError("n must be a positive integer.")
268
+ if alpha <= -1 or beta <= -1:
269
+ raise ValueError("alpha and beta must be greater than -1.")
270
+
271
+ if alpha == 0.0 and beta == 0.0:
272
+ return roots_legendre(m, mu)
273
+ if alpha == beta:
274
+ return roots_gegenbauer(m, alpha+0.5, mu)
275
+
276
+ if (alpha + beta) <= 1000:
277
+ mu0 = 2.0**(alpha+beta+1) * _ufuncs.beta(alpha+1, beta+1)
278
+ else:
279
+ # Avoid overflows in pow and beta for very large parameters
280
+ mu0 = np.exp((alpha + beta + 1) * np.log(2.0)
281
+ + _ufuncs.betaln(alpha+1, beta+1))
282
+ a = alpha
283
+ b = beta
284
+ if a + b == 0.0:
285
+ def an_func(k):
286
+ return np.where(k == 0, (b - a) / (2 + a + b), 0.0)
287
+ else:
288
+ def an_func(k):
289
+ return np.where(
290
+ k == 0,
291
+ (b - a) / (2 + a + b),
292
+ (b * b - a * a) / ((2.0 * k + a + b) * (2.0 * k + a + b + 2))
293
+ )
294
+
295
+ def bn_func(k):
296
+ return (
297
+ 2.0 / (2.0 * k + a + b)
298
+ * np.sqrt((k + a) * (k + b) / (2 * k + a + b + 1))
299
+ * np.where(k == 1, 1.0, np.sqrt(k * (k + a + b) / (2.0 * k + a + b - 1)))
300
+ )
301
+
302
+ def f(n, x):
303
+ return _ufuncs.eval_jacobi(n, a, b, x)
304
+ def df(n, x):
305
+ return 0.5 * (n + a + b + 1) * _ufuncs.eval_jacobi(n - 1, a + 1, b + 1, x)
306
+ return _gen_roots_and_weights(m, mu0, an_func, bn_func, f, df, False, mu)
307
+
308
+
309
+ def jacobi(n, alpha, beta, monic=False):
310
+ r"""Jacobi polynomial.
311
+
312
+ Defined to be the solution of
313
+
314
+ .. math::
315
+
316
+ \begin{aligned}
317
+ (1 - x^2)\frac{d^2}{dx^2} P_n^{(\alpha, \beta)}(x)
318
+ &+ \left(\beta - \alpha - (\alpha + \beta + 2)x\right)
319
+ \frac{d}{dx} P_n^{(\alpha, \beta)}(x) \\
320
+ &+ n(n + \alpha + \beta + 1) P_n^{(\alpha, \beta)}(x) = 0
321
+ \end{aligned}
322
+
323
+ for :math:`\alpha, \beta > -1`; :math:`P_n^{(\alpha, \beta)}` is a
324
+ polynomial of degree :math:`n`.
325
+
326
+ Parameters
327
+ ----------
328
+ n : int
329
+ Degree of the polynomial.
330
+ alpha : float
331
+ Parameter, must be greater than -1.
332
+ beta : float
333
+ Parameter, must be greater than -1.
334
+ monic : bool, optional
335
+ If `True`, scale the leading coefficient to be 1. Default is
336
+ `False`.
337
+
338
+ Returns
339
+ -------
340
+ P : orthopoly1d
341
+ Jacobi polynomial.
342
+
343
+ Notes
344
+ -----
345
+ For fixed :math:`\alpha, \beta`, the polynomials
346
+ :math:`P_n^{(\alpha, \beta)}` are orthogonal over :math:`[-1, 1]`
347
+ with weight function :math:`(1 - x)^\alpha(1 + x)^\beta`.
348
+
349
+ References
350
+ ----------
351
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
352
+ Handbook of Mathematical Functions with Formulas,
353
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
354
+
355
+ Examples
356
+ --------
357
+ The Jacobi polynomials satisfy the recurrence relation:
358
+
359
+ .. math::
360
+ P_n^{(\alpha, \beta-1)}(x) - P_n^{(\alpha-1, \beta)}(x)
361
+ = P_{n-1}^{(\alpha, \beta)}(x)
362
+
363
+ This can be verified, for example, for :math:`\alpha = \beta = 2`
364
+ and :math:`n = 1` over the interval :math:`[-1, 1]`:
365
+
366
+ >>> import numpy as np
367
+ >>> from scipy.special import jacobi
368
+ >>> x = np.arange(-1.0, 1.0, 0.01)
369
+ >>> np.allclose(jacobi(0, 2, 2)(x),
370
+ ... jacobi(1, 2, 1)(x) - jacobi(1, 1, 2)(x))
371
+ True
372
+
373
+ Plot of the Jacobi polynomial :math:`P_5^{(\alpha, -0.5)}` for
374
+ different values of :math:`\alpha`:
375
+
376
+ >>> import matplotlib.pyplot as plt
377
+ >>> x = np.arange(-1.0, 1.0, 0.01)
378
+ >>> fig, ax = plt.subplots()
379
+ >>> ax.set_ylim(-2.0, 2.0)
380
+ >>> ax.set_title(r'Jacobi polynomials $P_5^{(\alpha, -0.5)}$')
381
+ >>> for alpha in np.arange(0, 4, 1):
382
+ ... ax.plot(x, jacobi(5, alpha, -0.5)(x), label=rf'$\alpha={alpha}$')
383
+ >>> plt.legend(loc='best')
384
+ >>> plt.show()
385
+
386
+ """
387
+ if n < 0:
388
+ raise ValueError("n must be nonnegative.")
389
+
390
+ def wfunc(x):
391
+ return (1 - x) ** alpha * (1 + x) ** beta
392
+ if n == 0:
393
+ return orthopoly1d([], [], 1.0, 1.0, wfunc, (-1, 1), monic,
394
+ eval_func=np.ones_like)
395
+ x, w, mu = roots_jacobi(n, alpha, beta, mu=True)
396
+ ab1 = alpha + beta + 1.0
397
+ hn = 2**ab1 / (2 * n + ab1) * _gam(n + alpha + 1)
398
+ hn *= _gam(n + beta + 1.0) / _gam(n + 1) / _gam(n + ab1)
399
+ kn = _gam(2 * n + ab1) / 2.0**n / _gam(n + 1) / _gam(n + ab1)
400
+ # here kn = coefficient on x^n term
401
+ p = orthopoly1d(x, w, hn, kn, wfunc, (-1, 1), monic,
402
+ lambda x: _ufuncs.eval_jacobi(n, alpha, beta, x))
403
+ return p
404
+
405
+ # Jacobi Polynomials shifted G_n(p,q,x)
406
+
407
+
408
+ def roots_sh_jacobi(n, p1, q1, mu=False):
409
+ """Gauss-Jacobi (shifted) quadrature.
410
+
411
+ Compute the sample points and weights for Gauss-Jacobi (shifted)
412
+ quadrature. The sample points are the roots of the nth degree
413
+ shifted Jacobi polynomial, :math:`G^{p,q}_n(x)`. These sample
414
+ points and weights correctly integrate polynomials of degree
415
+ :math:`2n - 1` or less over the interval :math:`[0, 1]` with
416
+ weight function :math:`w(x) = (1 - x)^{p-q} x^{q-1}`. See 22.2.2
417
+ in [AS]_ for details.
418
+
419
+ Parameters
420
+ ----------
421
+ n : int
422
+ quadrature order.
423
+ p1 : float
424
+ (p1 - q1) must be > -1.
425
+ q1 : float
426
+ q1 must be > 0.
427
+ mu : bool, optional
428
+ If True, return the sum of the weights, optional.
429
+
430
+ Returns
431
+ -------
432
+ x : ndarray
433
+ Sample points
434
+ w : ndarray
435
+ Weights
436
+ mu : float
437
+ Sum of the weights
438
+
439
+ See Also
440
+ --------
441
+ scipy.integrate.fixed_quad
442
+
443
+ References
444
+ ----------
445
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
446
+ Handbook of Mathematical Functions with Formulas,
447
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
448
+
449
+ """
450
+ if (p1-q1) <= -1 or q1 <= 0:
451
+ message = "(p - q) must be greater than -1, and q must be greater than 0."
452
+ raise ValueError(message)
453
+ x, w, m = roots_jacobi(n, p1-q1, q1-1, True)
454
+ x = (x + 1) / 2
455
+ scale = 2.0**p1
456
+ w /= scale
457
+ m /= scale
458
+ if mu:
459
+ return x, w, m
460
+ else:
461
+ return x, w
462
+
463
+
464
+ def sh_jacobi(n, p, q, monic=False):
465
+ r"""Shifted Jacobi polynomial.
466
+
467
+ Defined by
468
+
469
+ .. math::
470
+
471
+ G_n^{(p, q)}(x)
472
+ = \binom{2n + p - 1}{n}^{-1}P_n^{(p - q, q - 1)}(2x - 1),
473
+
474
+ where :math:`P_n^{(\cdot, \cdot)}` is the nth Jacobi polynomial.
475
+
476
+ Parameters
477
+ ----------
478
+ n : int
479
+ Degree of the polynomial.
480
+ p : float
481
+ Parameter, must have :math:`p > q - 1`.
482
+ q : float
483
+ Parameter, must be greater than 0.
484
+ monic : bool, optional
485
+ If `True`, scale the leading coefficient to be 1. Default is
486
+ `False`.
487
+
488
+ Returns
489
+ -------
490
+ G : orthopoly1d
491
+ Shifted Jacobi polynomial.
492
+
493
+ Notes
494
+ -----
495
+ For fixed :math:`p, q`, the polynomials :math:`G_n^{(p, q)}` are
496
+ orthogonal over :math:`[0, 1]` with weight function :math:`(1 -
497
+ x)^{p - q}x^{q - 1}`.
498
+
499
+ Examples
500
+ --------
501
+ Evaluate the shifted Jacobi polynomial :math:`G_3^{(2, 1)}` at
502
+ :math:`x = 0.5`:
503
+
504
+ >>> import numpy as np
505
+ >>> from scipy.special import binom, jacobi, sh_jacobi
506
+ >>> np.isclose(sh_jacobi(3, 2, 1)(0.5), -3/280)
507
+ True
508
+
509
+ The polynomial is related to the Jacobi polynomial
510
+ :math:`P_n^{(p - q, q - 1)}`:
511
+
512
+ >>> x = np.linspace(0, 1, 5)
513
+ >>> n, p, q = 3, 2, 1
514
+ >>> scale = 1 / binom(2*n + p - 1, n)
515
+ >>> np.allclose(sh_jacobi(n, p, q)(x),
516
+ ... scale * jacobi(n, p - q, q - 1)(2*x - 1))
517
+ True
518
+
519
+ Plot :math:`G_3^{(p, 1)}` for several values of :math:`p`:
520
+
521
+ >>> import matplotlib.pyplot as plt
522
+ >>> x = np.linspace(0, 1, 400)
523
+ >>> fig, ax = plt.subplots()
524
+ >>> for p in [1, 2, 3]:
525
+ ... ax.plot(x, sh_jacobi(3, p, 1)(x), label=rf"$p={p}$")
526
+ >>> ax.set_title(r"Shifted Jacobi polynomials $G_3^{(p, 1)}$")
527
+ >>> ax.set_xlabel("x")
528
+ >>> ax.legend(loc="best")
529
+ >>> plt.show()
530
+
531
+ """
532
+ if n < 0:
533
+ raise ValueError("n must be nonnegative.")
534
+
535
+ def wfunc(x):
536
+ return (1.0 - x) ** (p - q) * x ** (q - 1.0)
537
+ if n == 0:
538
+ return orthopoly1d([], [], 1.0, 1.0, wfunc, (-1, 1), monic,
539
+ eval_func=np.ones_like)
540
+ n1 = n
541
+ x, w = roots_sh_jacobi(n1, p, q)
542
+ hn = _gam(n + 1) * _gam(n + q) * _gam(n + p) * _gam(n + p - q + 1)
543
+ hn /= (2 * n + p) * (_gam(2 * n + p)**2)
544
+ # kn = 1.0 in standard form so monic is redundant. Kept for compatibility.
545
+ kn = 1.0
546
+ pp = orthopoly1d(x, w, hn, kn, wfunc=wfunc, limits=(0, 1), monic=monic,
547
+ eval_func=lambda x: _ufuncs.eval_sh_jacobi(n, p, q, x))
548
+ return pp
549
+
550
+ # Generalized Laguerre L^(alpha)_n(x)
551
+
552
+
553
+ def roots_genlaguerre(n, alpha, mu=False):
554
+ r"""Gauss-generalized Laguerre quadrature.
555
+
556
+ Compute the sample points and weights for Gauss-generalized
557
+ Laguerre quadrature. The sample points are the roots of the nth
558
+ degree generalized Laguerre polynomial, :math:`L^{\alpha}_n(x)`.
559
+ These sample points and weights correctly integrate polynomials of
560
+ degree :math:`2n - 1` or less over the interval :math:`[0,
561
+ \infty]` with weight function :math:`w(x) = x^{\alpha}
562
+ e^{-x}`. See 22.3.9 in [AS]_ for details.
563
+
564
+ Parameters
565
+ ----------
566
+ n : int
567
+ quadrature order
568
+ alpha : float
569
+ alpha must be > -1
570
+ mu : bool, optional
571
+ If True, return the sum of the weights, optional.
572
+
573
+ Returns
574
+ -------
575
+ x : ndarray
576
+ Sample points
577
+ w : ndarray
578
+ Weights
579
+ mu : float
580
+ Sum of the weights
581
+
582
+ See Also
583
+ --------
584
+ scipy.integrate.fixed_quad
585
+
586
+ References
587
+ ----------
588
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
589
+ Handbook of Mathematical Functions with Formulas,
590
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
591
+
592
+ """
593
+ m = int(n)
594
+ if n < 1 or n != m:
595
+ raise ValueError("n must be a positive integer.")
596
+ if alpha < -1:
597
+ raise ValueError("alpha must be greater than -1.")
598
+
599
+ mu0 = _ufuncs.gamma(alpha + 1)
600
+
601
+ if m == 1:
602
+ x = np.array([alpha+1.0], 'd')
603
+ w = np.array([mu0], 'd')
604
+ if mu:
605
+ return x, w, mu0
606
+ else:
607
+ return x, w
608
+
609
+ def an_func(k):
610
+ return 2 * k + alpha + 1
611
+ def bn_func(k):
612
+ return -np.sqrt(k * (k + alpha))
613
+ def f(n, x):
614
+ return _ufuncs.eval_genlaguerre(n, alpha, x)
615
+ def df(n, x):
616
+ return (n * _ufuncs.eval_genlaguerre(n, alpha, x)
617
+ - (n + alpha) * _ufuncs.eval_genlaguerre(n - 1, alpha, x)) / x
618
+ return _gen_roots_and_weights(m, mu0, an_func, bn_func, f, df, False, mu)
619
+
620
+
621
+ def genlaguerre(n, alpha, monic=False):
622
+ r"""Generalized (associated) Laguerre polynomial.
623
+
624
+ Defined to be the solution of
625
+
626
+ .. math::
627
+ x\frac{d^2}{dx^2}L_n^{(\alpha)}(x)
628
+ + (\alpha + 1 - x)\frac{d}{dx}L_n^{(\alpha)}(x)
629
+ + nL_n^{(\alpha)}(x) = 0,
630
+
631
+ where :math:`\alpha > -1`; :math:`L_n^{(\alpha)}` is a polynomial
632
+ of degree :math:`n`.
633
+
634
+ Parameters
635
+ ----------
636
+ n : int
637
+ Degree of the polynomial.
638
+ alpha : float
639
+ Parameter, must be greater than -1.
640
+ monic : bool, optional
641
+ If `True`, scale the leading coefficient to be 1. Default is
642
+ `False`.
643
+
644
+ Returns
645
+ -------
646
+ L : orthopoly1d
647
+ Generalized Laguerre polynomial.
648
+
649
+ See Also
650
+ --------
651
+ laguerre : Laguerre polynomial.
652
+ hyp1f1 : confluent hypergeometric function
653
+
654
+ Notes
655
+ -----
656
+ For fixed :math:`\alpha`, the polynomials :math:`L_n^{(\alpha)}`
657
+ are orthogonal over :math:`[0, \infty)` with weight function
658
+ :math:`e^{-x}x^\alpha`.
659
+
660
+ The Laguerre polynomials are the special case where :math:`\alpha
661
+ = 0`.
662
+
663
+ References
664
+ ----------
665
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
666
+ Handbook of Mathematical Functions with Formulas,
667
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
668
+
669
+ Examples
670
+ --------
671
+ The generalized Laguerre polynomials are closely related to the confluent
672
+ hypergeometric function :math:`{}_1F_1`:
673
+
674
+ .. math::
675
+ L_n^{(\alpha)}(x) = \binom{n + \alpha}{n} {}_1F_1(-n, \alpha +1, x)
676
+
677
+ This can be verified, for example, for :math:`n = \alpha = 3` over the
678
+ interval :math:`[-1, 1]`:
679
+
680
+ >>> import numpy as np
681
+ >>> from scipy.special import binom, genlaguerre, hyp1f1
682
+ >>> x = np.arange(-1.0, 1.0, 0.01)
683
+ >>> np.allclose(genlaguerre(3, 3)(x), binom(6, 3) * hyp1f1(-3, 4, x))
684
+ True
685
+
686
+ This is the plot of the generalized Laguerre polynomials
687
+ :math:`L_3^{(\alpha)}` for some values of :math:`\alpha`:
688
+
689
+ >>> import matplotlib.pyplot as plt
690
+ >>> x = np.arange(-4.0, 12.0, 0.01)
691
+ >>> fig, ax = plt.subplots()
692
+ >>> ax.set_ylim(-5.0, 10.0)
693
+ >>> ax.set_title(r'Generalized Laguerre polynomials $L_3^{(\alpha)}$')
694
+ >>> for alpha in np.arange(0, 5):
695
+ ... ax.plot(x, genlaguerre(3, alpha)(x), label=rf"$L_3^{{({alpha})}}$")
696
+ >>> plt.legend(loc='best')
697
+ >>> plt.show()
698
+
699
+ """
700
+ if alpha <= -1:
701
+ raise ValueError("alpha must be > -1")
702
+ if n < 0:
703
+ raise ValueError("n must be nonnegative.")
704
+
705
+ if n == 0:
706
+ n1 = n + 1
707
+ else:
708
+ n1 = n
709
+ x, w = roots_genlaguerre(n1, alpha)
710
+ def wfunc(x):
711
+ return exp(-x) * x ** alpha
712
+ if n == 0:
713
+ x, w = [], []
714
+ hn = _gam(n + alpha + 1) / _gam(n + 1)
715
+ kn = (-1)**n / _gam(n + 1)
716
+ p = orthopoly1d(x, w, hn, kn, wfunc, (0, inf), monic,
717
+ lambda x: _ufuncs.eval_genlaguerre(n, alpha, x))
718
+ return p
719
+
720
+ # Laguerre L_n(x)
721
+
722
+
723
+ def roots_laguerre(n, mu=False):
724
+ r"""Gauss-Laguerre quadrature.
725
+
726
+ Compute the sample points and weights for Gauss-Laguerre
727
+ quadrature. The sample points are the roots of the nth degree
728
+ Laguerre polynomial, :math:`L_n(x)`. These sample points and
729
+ weights correctly integrate polynomials of degree :math:`2n - 1`
730
+ or less over the interval :math:`[0, \infty]` with weight function
731
+ :math:`w(x) = e^{-x}`. See 22.2.13 in [AS]_ for details.
732
+
733
+ Parameters
734
+ ----------
735
+ n : int
736
+ quadrature order
737
+ mu : bool, optional
738
+ If True, return the sum of the weights, optional.
739
+
740
+ Returns
741
+ -------
742
+ x : ndarray
743
+ Sample points
744
+ w : ndarray
745
+ Weights
746
+ mu : float
747
+ Sum of the weights
748
+
749
+ See Also
750
+ --------
751
+ scipy.integrate.fixed_quad
752
+ numpy.polynomial.laguerre.laggauss
753
+
754
+ References
755
+ ----------
756
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
757
+ Handbook of Mathematical Functions with Formulas,
758
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
759
+
760
+ """
761
+ return roots_genlaguerre(n, 0.0, mu=mu)
762
+
763
+
764
+ def laguerre(n, monic=False):
765
+ r"""Laguerre polynomial.
766
+
767
+ Defined to be the solution of
768
+
769
+ .. math::
770
+ x\frac{d^2}{dx^2}L_n(x) + (1 - x)\frac{d}{dx}L_n(x)
771
+ + nL_n(x) = 0;
772
+
773
+ :math:`L_n` is a polynomial of degree :math:`n`.
774
+
775
+ Parameters
776
+ ----------
777
+ n : int
778
+ Degree of the polynomial.
779
+ monic : bool, optional
780
+ If `True`, scale the leading coefficient to be 1. Default is
781
+ `False`.
782
+
783
+ Returns
784
+ -------
785
+ L : orthopoly1d
786
+ Laguerre Polynomial.
787
+
788
+ See Also
789
+ --------
790
+ genlaguerre : Generalized (associated) Laguerre polynomial.
791
+
792
+ Notes
793
+ -----
794
+ The polynomials :math:`L_n` are orthogonal over :math:`[0,
795
+ \infty)` with weight function :math:`e^{-x}`.
796
+
797
+ References
798
+ ----------
799
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
800
+ Handbook of Mathematical Functions with Formulas,
801
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
802
+
803
+ Examples
804
+ --------
805
+ The Laguerre polynomials :math:`L_n` are the special case
806
+ :math:`\alpha = 0` of the generalized Laguerre polynomials
807
+ :math:`L_n^{(\alpha)}`.
808
+ Let's verify it on the interval :math:`[-1, 1]`:
809
+
810
+ >>> import numpy as np
811
+ >>> from scipy.special import genlaguerre
812
+ >>> from scipy.special import laguerre
813
+ >>> x = np.arange(-1.0, 1.0, 0.01)
814
+ >>> np.allclose(genlaguerre(3, 0)(x), laguerre(3)(x))
815
+ True
816
+
817
+ The polynomials :math:`L_n` also satisfy the recurrence relation:
818
+
819
+ .. math::
820
+ (n + 1)L_{n+1}(x) = (2n +1 -x)L_n(x) - nL_{n-1}(x)
821
+
822
+ This can be easily checked on :math:`[0, 1]` for :math:`n = 3`:
823
+
824
+ >>> x = np.arange(0.0, 1.0, 0.01)
825
+ >>> np.allclose(4 * laguerre(4)(x),
826
+ ... (7 - x) * laguerre(3)(x) - 3 * laguerre(2)(x))
827
+ True
828
+
829
+ This is the plot of the first few Laguerre polynomials :math:`L_n`:
830
+
831
+ >>> import matplotlib.pyplot as plt
832
+ >>> x = np.arange(-1.0, 5.0, 0.01)
833
+ >>> fig, ax = plt.subplots()
834
+ >>> ax.set_ylim(-5.0, 5.0)
835
+ >>> ax.set_title(r'Laguerre polynomials $L_n$')
836
+ >>> for n in np.arange(0, 5):
837
+ ... ax.plot(x, laguerre(n)(x), label=rf'$L_{n}$')
838
+ >>> plt.legend(loc='best')
839
+ >>> plt.show()
840
+
841
+ """
842
+ if n < 0:
843
+ raise ValueError("n must be nonnegative.")
844
+
845
+ if n == 0:
846
+ n1 = n + 1
847
+ else:
848
+ n1 = n
849
+ x, w = roots_laguerre(n1)
850
+ if n == 0:
851
+ x, w = [], []
852
+ hn = 1.0
853
+ kn = (-1)**n / _gam(n + 1)
854
+ p = orthopoly1d(x, w, hn, kn, lambda x: exp(-x), (0, inf), monic,
855
+ lambda x: _ufuncs.eval_laguerre(n, x))
856
+ return p
857
+
858
+ # Hermite 1 H_n(x)
859
+
860
+
861
+ def roots_hermite(n, mu=False):
862
+ r"""Gauss-Hermite (physicist's) quadrature.
863
+
864
+ Compute the sample points and weights for Gauss-Hermite
865
+ quadrature. The sample points are the roots of the nth degree
866
+ Hermite polynomial, :math:`H_n(x)`. These sample points and
867
+ weights correctly integrate polynomials of degree :math:`2n - 1`
868
+ or less over the interval :math:`[-\infty, \infty]` with weight
869
+ function :math:`w(x) = e^{-x^2}`. See 22.2.14 in [AS]_ for
870
+ details.
871
+
872
+ Parameters
873
+ ----------
874
+ n : int
875
+ quadrature order
876
+ mu : bool, optional
877
+ If True, return the sum of the weights, optional.
878
+
879
+ Returns
880
+ -------
881
+ x : ndarray
882
+ Sample points
883
+ w : ndarray
884
+ Weights
885
+ mu : float
886
+ Sum of the weights
887
+
888
+ See Also
889
+ --------
890
+ scipy.integrate.fixed_quad
891
+ numpy.polynomial.hermite.hermgauss
892
+ roots_hermitenorm
893
+
894
+ Notes
895
+ -----
896
+ For small n up to 150 a modified version of the Golub-Welsch
897
+ algorithm is used. Nodes are computed from the eigenvalue
898
+ problem and improved by one step of a Newton iteration.
899
+ The weights are computed from the well-known analytical formula.
900
+
901
+ For n larger than 150 an optimal asymptotic algorithm is applied
902
+ which computes nodes and weights in a numerically stable manner.
903
+ The algorithm has linear runtime making computation for very
904
+ large n (several thousand or more) feasible.
905
+
906
+ References
907
+ ----------
908
+ .. [townsend.trogdon.olver-2014]
909
+ Townsend, A. and Trogdon, T. and Olver, S. (2014)
910
+ *Fast computation of Gauss quadrature nodes and
911
+ weights on the whole real line*. :arXiv:`1410.5286`.
912
+ .. [townsend.trogdon.olver-2015]
913
+ Townsend, A. and Trogdon, T. and Olver, S. (2015)
914
+ *Fast computation of Gauss quadrature nodes and
915
+ weights on the whole real line*.
916
+ IMA Journal of Numerical Analysis
917
+ :doi:`10.1093/imanum/drv002`.
918
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
919
+ Handbook of Mathematical Functions with Formulas,
920
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
921
+
922
+ """
923
+ m = int(n)
924
+ if n < 1 or n != m:
925
+ raise ValueError("n must be a positive integer.")
926
+
927
+ mu0 = np.sqrt(np.pi)
928
+ if n <= 150:
929
+ def an_func(k):
930
+ return 0.0 * k
931
+ def bn_func(k):
932
+ return np.sqrt(k / 2.0)
933
+ f = _ufuncs.eval_hermite
934
+ def df(n, x):
935
+ return 2.0 * n * _ufuncs.eval_hermite(n - 1, x)
936
+ return _gen_roots_and_weights(m, mu0, an_func, bn_func, f, df, True, mu)
937
+ else:
938
+ nodes, weights = _roots_hermite_asy(m)
939
+ if mu:
940
+ return nodes, weights, mu0
941
+ else:
942
+ return nodes, weights
943
+
944
+
945
+ def _compute_tauk(n, k, maxit=5):
946
+ """Helper function for Tricomi initial guesses
947
+
948
+ For details, see formula 3.1 in lemma 3.1 in the
949
+ original paper.
950
+
951
+ Parameters
952
+ ----------
953
+ n : int
954
+ Quadrature order
955
+ k : ndarray of type int
956
+ Index of roots :math:`\tau_k` to compute
957
+ maxit : int
958
+ Number of Newton maxit performed, the default
959
+ value of 5 is sufficient.
960
+
961
+ Returns
962
+ -------
963
+ tauk : ndarray
964
+ Roots of equation 3.1
965
+
966
+ See Also
967
+ --------
968
+ initial_nodes_a
969
+ roots_hermite_asy
970
+ """
971
+ a = n % 2 - 0.5
972
+ c = (4.0*floor(n/2.0) - 4.0*k + 3.0)*pi / (4.0*floor(n/2.0) + 2.0*a + 2.0)
973
+ def f(x):
974
+ return x - sin(x) - c
975
+ def df(x):
976
+ return 1.0 - cos(x)
977
+ xi = 0.5*pi
978
+ for i in range(maxit):
979
+ xi = xi - f(xi)/df(xi)
980
+ return xi
981
+
982
+
983
+ def _initial_nodes_a(n, k):
984
+ r"""Tricomi initial guesses
985
+
986
+ Computes an initial approximation to the square of the `k`-th
987
+ (positive) root :math:`x_k` of the Hermite polynomial :math:`H_n`
988
+ of order :math:`n`. The formula is the one from lemma 3.1 in the
989
+ original paper. The guesses are accurate except in the region
990
+ near :math:`\sqrt{2n + 1}`.
991
+
992
+ Parameters
993
+ ----------
994
+ n : int
995
+ Quadrature order
996
+ k : ndarray of type int
997
+ Index of roots to compute
998
+
999
+ Returns
1000
+ -------
1001
+ xksq : ndarray
1002
+ Square of the approximate roots
1003
+
1004
+ See Also
1005
+ --------
1006
+ initial_nodes
1007
+ roots_hermite_asy
1008
+ """
1009
+ tauk = _compute_tauk(n, k)
1010
+ sigk = cos(0.5*tauk)**2
1011
+ a = n % 2 - 0.5
1012
+ nu = 4.0*floor(n/2.0) + 2.0*a + 2.0
1013
+ # Initial approximation of Hermite roots (square)
1014
+ xksq = nu*sigk - 1.0/(3.0*nu) * (5.0/(4.0*(1.0-sigk)**2) - 1.0/(1.0-sigk) - 0.25)
1015
+ return xksq
1016
+
1017
+
1018
+ def _initial_nodes_b(n, k):
1019
+ r"""Gatteschi initial guesses
1020
+
1021
+ Computes an initial approximation to the square of the kth
1022
+ (positive) root :math:`x_k` of the Hermite polynomial :math:`H_n`
1023
+ of order :math:`n`. The formula is the one from lemma 3.2 in the
1024
+ original paper. The guesses are accurate in the region just
1025
+ below :math:`\sqrt{2n + 1}`.
1026
+
1027
+ Parameters
1028
+ ----------
1029
+ n : int
1030
+ Quadrature order
1031
+ k : ndarray of type int
1032
+ Index of roots to compute
1033
+
1034
+ Returns
1035
+ -------
1036
+ xksq : ndarray
1037
+ Square of the approximate root
1038
+
1039
+ See Also
1040
+ --------
1041
+ initial_nodes
1042
+ roots_hermite_asy
1043
+ """
1044
+ a = n % 2 - 0.5
1045
+ nu = 4.0*floor(n/2.0) + 2.0*a + 2.0
1046
+ # Airy roots by approximation
1047
+ ak = _specfun.airyzo(k.max(), 1)[0][::-1]
1048
+ # Initial approximation of Hermite roots (square)
1049
+ xksq = (nu
1050
+ + 2.0**(2.0/3.0) * ak * nu**(1.0/3.0)
1051
+ + 1.0/5.0 * 2.0**(4.0/3.0) * ak**2 * nu**(-1.0/3.0)
1052
+ + (9.0/140.0 - 12.0/175.0 * ak**3) * nu**(-1.0)
1053
+ + (16.0/1575.0 * ak + 92.0/7875.0 * ak**4) * 2.0**(2.0/3.0) * nu**(-5.0/3.0)
1054
+ - (15152.0/3031875.0 * ak**5 + 1088.0/121275.0 * ak**2)
1055
+ * 2.0**(1.0/3.0) * nu**(-7.0/3.0))
1056
+ return xksq
1057
+
1058
+
1059
+ def _initial_nodes(n):
1060
+ """Initial guesses for the Hermite roots
1061
+
1062
+ Computes an initial approximation to the non-negative
1063
+ roots :math:`x_k` of the Hermite polynomial :math:`H_n`
1064
+ of order :math:`n`. The Tricomi and Gatteschi initial
1065
+ guesses are used in the region where they are accurate.
1066
+
1067
+ Parameters
1068
+ ----------
1069
+ n : int
1070
+ Quadrature order
1071
+
1072
+ Returns
1073
+ -------
1074
+ xk : ndarray
1075
+ Approximate roots
1076
+
1077
+ See Also
1078
+ --------
1079
+ roots_hermite_asy
1080
+ """
1081
+ # Turnover point
1082
+ # linear polynomial fit to error of 10, 25, 40, ..., 1000 point rules
1083
+ fit = 0.49082003*n - 4.37859653
1084
+ turnover = around(fit).astype(int)
1085
+ # Compute all approximations
1086
+ ia = arange(1, int(floor(n*0.5)+1))
1087
+ ib = ia[::-1]
1088
+ xasq = _initial_nodes_a(n, ia[:turnover+1])
1089
+ xbsq = _initial_nodes_b(n, ib[turnover+1:])
1090
+ # Combine
1091
+ iv = sqrt(hstack([xasq, xbsq]))
1092
+ # Central node is always zero
1093
+ if n % 2 == 1:
1094
+ iv = hstack([0.0, iv])
1095
+ return iv
1096
+
1097
+
1098
+ def _pbcf(n, theta):
1099
+ r"""Asymptotic series expansion of parabolic cylinder function
1100
+
1101
+ The implementation is based on sections 3.2 and 3.3 from the
1102
+ original paper. Compared to the published version this code
1103
+ adds one more term to the asymptotic series. The detailed
1104
+ formulas can be found at [parabolic-asymptotics]_. The evaluation
1105
+ is done in a transformed variable :math:`\theta := \arccos(t)`
1106
+ where :math:`t := x / \mu` and :math:`\mu := \sqrt{2n + 1}`.
1107
+
1108
+ Parameters
1109
+ ----------
1110
+ n : int
1111
+ Quadrature order
1112
+ theta : ndarray
1113
+ Transformed position variable
1114
+
1115
+ Returns
1116
+ -------
1117
+ U : ndarray
1118
+ Value of the parabolic cylinder function :math:`U(a, \theta)`.
1119
+ Ud : ndarray
1120
+ Value of the derivative :math:`U^{\prime}(a, \theta)` of
1121
+ the parabolic cylinder function.
1122
+
1123
+ See Also
1124
+ --------
1125
+ roots_hermite_asy
1126
+
1127
+ References
1128
+ ----------
1129
+ .. [parabolic-asymptotics]
1130
+ https://dlmf.nist.gov/12.10#vii
1131
+ """
1132
+ st = sin(theta)
1133
+ ct = cos(theta)
1134
+ # https://dlmf.nist.gov/12.10#vii
1135
+ mu = 2.0*n + 1.0
1136
+ # https://dlmf.nist.gov/12.10#E23
1137
+ eta = 0.5*theta - 0.5*st*ct
1138
+ # https://dlmf.nist.gov/12.10#E39
1139
+ zeta = -(3.0*eta/2.0) ** (2.0/3.0)
1140
+ # https://dlmf.nist.gov/12.10#E40
1141
+ phi = (-zeta / st**2) ** (0.25)
1142
+ # Coefficients
1143
+ # https://dlmf.nist.gov/12.10#E43
1144
+ a0 = 1.0
1145
+ a1 = 0.10416666666666666667
1146
+ a2 = 0.08355034722222222222
1147
+ a3 = 0.12822657455632716049
1148
+ a4 = 0.29184902646414046425
1149
+ a5 = 0.88162726744375765242
1150
+ b0 = 1.0
1151
+ b1 = -0.14583333333333333333
1152
+ b2 = -0.09874131944444444444
1153
+ b3 = -0.14331205391589506173
1154
+ b4 = -0.31722720267841354810
1155
+ b5 = -0.94242914795712024914
1156
+ # Polynomials
1157
+ # https://dlmf.nist.gov/12.10#E9
1158
+ # https://dlmf.nist.gov/12.10#E10
1159
+ ctp = ct ** arange(16).reshape((-1,1))
1160
+ u0 = 1.0
1161
+ u1 = (1.0*ctp[3,:] - 6.0*ct) / 24.0
1162
+ u2 = (-9.0*ctp[4,:] + 249.0*ctp[2,:] + 145.0) / 1152.0
1163
+ u3 = (-4042.0*ctp[9,:] + 18189.0*ctp[7,:] - 28287.0*ctp[5,:]
1164
+ - 151995.0*ctp[3,:] - 259290.0*ct) / 414720.0
1165
+ u4 = (72756.0*ctp[10,:] - 321339.0*ctp[8,:] - 154982.0*ctp[6,:]
1166
+ + 50938215.0*ctp[4,:] + 122602962.0*ctp[2,:] + 12773113.0) / 39813120.0
1167
+ u5 = (82393456.0*ctp[15,:] - 617950920.0*ctp[13,:] + 1994971575.0*ctp[11,:]
1168
+ - 3630137104.0*ctp[9,:] + 4433574213.0*ctp[7,:] - 37370295816.0*ctp[5,:]
1169
+ - 119582875013.0*ctp[3,:] - 34009066266.0*ct) / 6688604160.0
1170
+ v0 = 1.0
1171
+ v1 = (1.0*ctp[3,:] + 6.0*ct) / 24.0
1172
+ v2 = (15.0*ctp[4,:] - 327.0*ctp[2,:] - 143.0) / 1152.0
1173
+ v3 = (-4042.0*ctp[9,:] + 18189.0*ctp[7,:] - 36387.0*ctp[5,:]
1174
+ + 238425.0*ctp[3,:] + 259290.0*ct) / 414720.0
1175
+ v4 = (-121260.0*ctp[10,:] + 551733.0*ctp[8,:] - 151958.0*ctp[6,:]
1176
+ - 57484425.0*ctp[4,:] - 132752238.0*ctp[2,:] - 12118727) / 39813120.0
1177
+ v5 = (82393456.0*ctp[15,:] - 617950920.0*ctp[13,:] + 2025529095.0*ctp[11,:]
1178
+ - 3750839308.0*ctp[9,:] + 3832454253.0*ctp[7,:] + 35213253348.0*ctp[5,:]
1179
+ + 130919230435.0*ctp[3,:] + 34009066266*ct) / 6688604160.0
1180
+ # Airy Evaluation (Bi and Bip unused)
1181
+ Ai, Aip, Bi, Bip = airy(mu**(4.0/6.0) * zeta)
1182
+ # Prefactor for U
1183
+ P = 2.0*sqrt(pi) * mu**(1.0/6.0) * phi
1184
+ # Terms for U
1185
+ # https://dlmf.nist.gov/12.10#E42
1186
+ phip = phi ** arange(6, 31, 6).reshape((-1,1))
1187
+ A0 = b0*u0
1188
+ A1 = (b2*u0 + phip[0,:]*b1*u1 + phip[1,:]*b0*u2) / zeta**3
1189
+ A2 = (b4*u0 + phip[0,:]*b3*u1 + phip[1,:]*b2*u2 + phip[2,:]*b1*u3
1190
+ + phip[3,:]*b0*u4) / zeta**6
1191
+ B0 = -(a1*u0 + phip[0,:]*a0*u1) / zeta**2
1192
+ B1 = -(a3*u0 + phip[0,:]*a2*u1 + phip[1,:]*a1*u2 + phip[2,:]*a0*u3) / zeta**5
1193
+ B2 = -(a5*u0 + phip[0,:]*a4*u1 + phip[1,:]*a3*u2 + phip[2,:]*a2*u3
1194
+ + phip[3,:]*a1*u4 + phip[4,:]*a0*u5) / zeta**8
1195
+ # U
1196
+ # https://dlmf.nist.gov/12.10#E35
1197
+ U = P * (Ai * (A0 + A1/mu**2.0 + A2/mu**4.0) +
1198
+ Aip * (B0 + B1/mu**2.0 + B2/mu**4.0) / mu**(8.0/6.0))
1199
+ # Prefactor for derivative of U
1200
+ Pd = sqrt(2.0*pi) * mu**(2.0/6.0) / phi
1201
+ # Terms for derivative of U
1202
+ # https://dlmf.nist.gov/12.10#E46
1203
+ C0 = -(b1*v0 + phip[0,:]*b0*v1) / zeta
1204
+ C1 = -(b3*v0 + phip[0,:]*b2*v1 + phip[1,:]*b1*v2 + phip[2,:]*b0*v3) / zeta**4
1205
+ C2 = -(b5*v0 + phip[0,:]*b4*v1 + phip[1,:]*b3*v2 + phip[2,:]*b2*v3
1206
+ + phip[3,:]*b1*v4 + phip[4,:]*b0*v5) / zeta**7
1207
+ D0 = a0*v0
1208
+ D1 = (a2*v0 + phip[0,:]*a1*v1 + phip[1,:]*a0*v2) / zeta**3
1209
+ D2 = (a4*v0 + phip[0,:]*a3*v1 + phip[1,:]*a2*v2 + phip[2,:]*a1*v3
1210
+ + phip[3,:]*a0*v4) / zeta**6
1211
+ # Derivative of U
1212
+ # https://dlmf.nist.gov/12.10#E36
1213
+ Ud = Pd * (Ai * (C0 + C1/mu**2.0 + C2/mu**4.0) / mu**(4.0/6.0) +
1214
+ Aip * (D0 + D1/mu**2.0 + D2/mu**4.0))
1215
+ return U, Ud
1216
+
1217
+
1218
+ def _newton(n, x_initial, maxit=5):
1219
+ """Newton iteration for polishing the asymptotic approximation
1220
+ to the zeros of the Hermite polynomials.
1221
+
1222
+ Parameters
1223
+ ----------
1224
+ n : int
1225
+ Quadrature order
1226
+ x_initial : ndarray
1227
+ Initial guesses for the roots
1228
+ maxit : int
1229
+ Maximal number of Newton iterations.
1230
+ The default 5 is sufficient, usually
1231
+ only one or two steps are needed.
1232
+
1233
+ Returns
1234
+ -------
1235
+ nodes : ndarray
1236
+ Quadrature nodes
1237
+ weights : ndarray
1238
+ Quadrature weights
1239
+
1240
+ See Also
1241
+ --------
1242
+ roots_hermite_asy
1243
+ """
1244
+ # Variable transformation
1245
+ mu = sqrt(2.0*n + 1.0)
1246
+ t = x_initial / mu
1247
+ theta = arccos(t)
1248
+ # Newton iteration
1249
+ for i in range(maxit):
1250
+ u, ud = _pbcf(n, theta)
1251
+ dtheta = u / (sqrt(2.0) * mu * sin(theta) * ud)
1252
+ theta = theta + dtheta
1253
+ if max(abs(dtheta)) < 1e-14:
1254
+ break
1255
+ # Undo variable transformation
1256
+ x = mu * cos(theta)
1257
+ # Central node is always zero
1258
+ if n % 2 == 1:
1259
+ x[0] = 0.0
1260
+ # Compute weights
1261
+ w = exp(-x**2) / (2.0*ud**2)
1262
+ return x, w
1263
+
1264
+
1265
+ def _roots_hermite_asy(n):
1266
+ r"""Gauss-Hermite (physicist's) quadrature for large n.
1267
+
1268
+ Computes the sample points and weights for Gauss-Hermite quadrature.
1269
+ The sample points are the roots of the nth degree Hermite polynomial,
1270
+ :math:`H_n(x)`. These sample points and weights correctly integrate
1271
+ polynomials of degree :math:`2n - 1` or less over the interval
1272
+ :math:`[-\infty, \infty]` with weight function :math:`f(x) = e^{-x^2}`.
1273
+
1274
+ This method relies on asymptotic expansions which work best for n > 150.
1275
+ The algorithm has linear runtime making computation for very large n
1276
+ feasible.
1277
+
1278
+ Parameters
1279
+ ----------
1280
+ n : int
1281
+ quadrature order
1282
+
1283
+ Returns
1284
+ -------
1285
+ nodes : ndarray
1286
+ Quadrature nodes
1287
+ weights : ndarray
1288
+ Quadrature weights
1289
+
1290
+ See Also
1291
+ --------
1292
+ roots_hermite
1293
+
1294
+ References
1295
+ ----------
1296
+ .. [townsend.trogdon.olver-2014]
1297
+ Townsend, A. and Trogdon, T. and Olver, S. (2014)
1298
+ *Fast computation of Gauss quadrature nodes and
1299
+ weights on the whole real line*. :arXiv:`1410.5286`.
1300
+
1301
+ .. [townsend.trogdon.olver-2015]
1302
+ Townsend, A. and Trogdon, T. and Olver, S. (2015)
1303
+ *Fast computation of Gauss quadrature nodes and
1304
+ weights on the whole real line*.
1305
+ IMA Journal of Numerical Analysis
1306
+ :doi:`10.1093/imanum/drv002`.
1307
+ """
1308
+ iv = _initial_nodes(n)
1309
+ nodes, weights = _newton(n, iv)
1310
+ # Combine with negative parts
1311
+ if n % 2 == 0:
1312
+ nodes = hstack([-nodes[::-1], nodes])
1313
+ weights = hstack([weights[::-1], weights])
1314
+ else:
1315
+ nodes = hstack([-nodes[-1:0:-1], nodes])
1316
+ weights = hstack([weights[-1:0:-1], weights])
1317
+ # Scale weights
1318
+ weights *= sqrt(pi) / sum(weights)
1319
+ return nodes, weights
1320
+
1321
+
1322
+ def hermite(n, monic=False):
1323
+ r"""Physicist's Hermite polynomial.
1324
+
1325
+ Defined by
1326
+
1327
+ .. math::
1328
+
1329
+ H_n(x) = (-1)^ne^{x^2}\frac{d^n}{dx^n}e^{-x^2};
1330
+
1331
+ :math:`H_n` is a polynomial of degree :math:`n`.
1332
+
1333
+ Parameters
1334
+ ----------
1335
+ n : int
1336
+ Degree of the polynomial.
1337
+ monic : bool, optional
1338
+ If `True`, scale the leading coefficient to be 1. Default is
1339
+ `False`.
1340
+
1341
+ Returns
1342
+ -------
1343
+ H : orthopoly1d
1344
+ Physicist's Hermite polynomial.
1345
+
1346
+ Notes
1347
+ -----
1348
+ The polynomials :math:`H_n` are orthogonal over :math:`(-\infty,
1349
+ \infty)` with weight function :math:`e^{-x^2}`.
1350
+
1351
+ Examples
1352
+ --------
1353
+ >>> from scipy import special
1354
+ >>> import matplotlib.pyplot as plt
1355
+ >>> import numpy as np
1356
+
1357
+ >>> p_monic = special.hermite(3, monic=True)
1358
+ >>> p_monic
1359
+ poly1d([ 1. , 0. , -1.5, 0. ])
1360
+ >>> p_monic(1)
1361
+ -0.49999999999999983
1362
+ >>> x = np.linspace(-3, 3, 400)
1363
+ >>> y = p_monic(x)
1364
+ >>> plt.plot(x, y)
1365
+ >>> plt.title("Monic Hermite polynomial of degree 3")
1366
+ >>> plt.xlabel("x")
1367
+ >>> plt.ylabel("H_3(x)")
1368
+ >>> plt.show()
1369
+
1370
+ """
1371
+ if n < 0:
1372
+ raise ValueError("n must be nonnegative.")
1373
+
1374
+ if n == 0:
1375
+ n1 = n + 1
1376
+ else:
1377
+ n1 = n
1378
+ x, w = roots_hermite(n1)
1379
+ def wfunc(x):
1380
+ return exp(-x * x)
1381
+ if n == 0:
1382
+ x, w = [], []
1383
+ hn = 2**n * _gam(n + 1) * sqrt(pi)
1384
+ kn = 2**n
1385
+ p = orthopoly1d(x, w, hn, kn, wfunc, (-inf, inf), monic,
1386
+ lambda x: _ufuncs.eval_hermite(n, x))
1387
+ return p
1388
+
1389
+ # Hermite 2 He_n(x)
1390
+
1391
+
1392
+ def roots_hermitenorm(n, mu=False):
1393
+ r"""Gauss-Hermite (statistician's) quadrature.
1394
+
1395
+ Compute the sample points and weights for Gauss-Hermite
1396
+ quadrature. The sample points are the roots of the nth degree
1397
+ Hermite polynomial, :math:`He_n(x)`. These sample points and
1398
+ weights correctly integrate polynomials of degree :math:`2n - 1`
1399
+ or less over the interval :math:`[-\infty, \infty]` with weight
1400
+ function :math:`w(x) = e^{-x^2/2}`. See 22.2.15 in [AS]_ for more
1401
+ details.
1402
+
1403
+ Parameters
1404
+ ----------
1405
+ n : int
1406
+ quadrature order.
1407
+ mu : bool, optional
1408
+ If True, return the sum of the weights, optional.
1409
+
1410
+ Returns
1411
+ -------
1412
+ x : ndarray
1413
+ Sample points.
1414
+ w : ndarray
1415
+ Weights.
1416
+ mu : float
1417
+ Sum of the weights.
1418
+
1419
+ See Also
1420
+ --------
1421
+ scipy.integrate.fixed_quad
1422
+ numpy.polynomial.hermite_e.hermegauss
1423
+
1424
+ Notes
1425
+ -----
1426
+ For small n up to 150 a modified version of the Golub-Welsch
1427
+ algorithm is used. Nodes are computed from the eigenvalue
1428
+ problem and improved by one step of a Newton iteration.
1429
+ The weights are computed from the well-known analytical formula.
1430
+
1431
+ For n larger than 150 an optimal asymptotic algorithm is used
1432
+ which computes nodes and weights in a numerical stable manner.
1433
+ The algorithm has linear runtime making computation for very
1434
+ large n (several thousand or more) feasible.
1435
+
1436
+ References
1437
+ ----------
1438
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
1439
+ Handbook of Mathematical Functions with Formulas,
1440
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
1441
+
1442
+ """
1443
+ m = int(n)
1444
+ if n < 1 or n != m:
1445
+ raise ValueError("n must be a positive integer.")
1446
+
1447
+ mu0 = np.sqrt(2.0*np.pi)
1448
+ if n <= 150:
1449
+ def an_func(k):
1450
+ return 0.0 * k
1451
+ def bn_func(k):
1452
+ return np.sqrt(k)
1453
+ f = _ufuncs.eval_hermitenorm
1454
+ def df(n, x):
1455
+ return n * _ufuncs.eval_hermitenorm(n - 1, x)
1456
+ return _gen_roots_and_weights(m, mu0, an_func, bn_func, f, df, True, mu)
1457
+ else:
1458
+ nodes, weights = _roots_hermite_asy(m)
1459
+ # Transform
1460
+ nodes *= sqrt(2)
1461
+ weights *= sqrt(2)
1462
+ if mu:
1463
+ return nodes, weights, mu0
1464
+ else:
1465
+ return nodes, weights
1466
+
1467
+
1468
+ def hermitenorm(n, monic=False):
1469
+ r"""Probabilist's Hermite polynomial.
1470
+
1471
+ Defined by
1472
+
1473
+ .. math::
1474
+
1475
+ He_n(x) = (-1)^ne^{x^2/2}\frac{d^n}{dx^n}e^{-x^2/2};
1476
+
1477
+ :math:`He_n` is a polynomial of degree :math:`n`.
1478
+
1479
+ Parameters
1480
+ ----------
1481
+ n : int
1482
+ Degree of the polynomial.
1483
+ monic : bool, optional
1484
+ If `True`, scale the leading coefficient to be 1. Default is
1485
+ `False`.
1486
+
1487
+ Returns
1488
+ -------
1489
+ He : orthopoly1d
1490
+ Probabilist's Hermite polynomial.
1491
+
1492
+ Notes
1493
+ -----
1494
+
1495
+ The polynomials :math:`He_n` are orthogonal over :math:`(-\infty,
1496
+ \infty)` with weight function :math:`e^{-x^2/2}`.
1497
+
1498
+ Examples
1499
+ --------
1500
+ >>> import numpy as np
1501
+ >>> import matplotlib.pyplot as plt
1502
+ >>> from scipy.special import hermitenorm
1503
+
1504
+ >>> p_monic = hermitenorm(3)
1505
+ >>> p_monic
1506
+ poly1d([ 1., 0., -3., 0.])
1507
+
1508
+ Evaluate the probabilist's Hermite polynomial of degree 3 at x = 1:
1509
+
1510
+ >>> p_monic(1)
1511
+ np.float64(-2.0)
1512
+
1513
+ Plot probabilist's Hermite polynomials of degree 0 to 4:
1514
+
1515
+ >>> x = np.linspace(-3, 3, 100)
1516
+ >>> fig, ax = plt.subplots()
1517
+ >>> for i in range(5):
1518
+ ... ax.plot(x, hermitenorm(i)(x), label=f"n={i}")
1519
+ >>> plt.title(f"Probabilist's Hermite polynomials $He_n$")
1520
+ >>> plt.xlabel("x")
1521
+ >>> plt.ylabel(rf"$He_n(x)$")
1522
+ >>> plt.legend(loc="best")
1523
+ >>> plt.show()
1524
+
1525
+ """
1526
+ if n < 0:
1527
+ raise ValueError("n must be nonnegative.")
1528
+
1529
+ if n == 0:
1530
+ n1 = n + 1
1531
+ else:
1532
+ n1 = n
1533
+ x, w = roots_hermitenorm(n1)
1534
+ def wfunc(x):
1535
+ return exp(-x * x / 2.0)
1536
+ if n == 0:
1537
+ x, w = [], []
1538
+ hn = sqrt(2 * pi) * _gam(n + 1)
1539
+ kn = 1.0
1540
+ p = orthopoly1d(x, w, hn, kn, wfunc=wfunc, limits=(-inf, inf), monic=monic,
1541
+ eval_func=lambda x: _ufuncs.eval_hermitenorm(n, x))
1542
+ return p
1543
+
1544
+ # The remainder of the polynomials can be derived from the ones above.
1545
+
1546
+ # Ultraspherical (Gegenbauer) C^(alpha)_n(x)
1547
+
1548
+
1549
+ def roots_gegenbauer(n, alpha, mu=False):
1550
+ r"""Gauss-Gegenbauer quadrature.
1551
+
1552
+ Compute the sample points and weights for Gauss-Gegenbauer
1553
+ quadrature. The sample points are the roots of the nth degree
1554
+ Gegenbauer polynomial, :math:`C^{\alpha}_n(x)`. These sample
1555
+ points and weights correctly integrate polynomials of degree
1556
+ :math:`2n - 1` or less over the interval :math:`[-1, 1]` with
1557
+ weight function :math:`w(x) = (1 - x^2)^{\alpha - 1/2}`. See
1558
+ 22.2.3 in [AS]_ for more details.
1559
+
1560
+ Parameters
1561
+ ----------
1562
+ n : int
1563
+ quadrature order.
1564
+ alpha : float
1565
+ alpha must be > -0.5.
1566
+ mu : bool, optional
1567
+ If True, return the sum of the weights, optional.
1568
+
1569
+ Returns
1570
+ -------
1571
+ x : ndarray
1572
+ Sample points
1573
+ w : ndarray
1574
+ Weights
1575
+ mu : float
1576
+ Sum of the weights
1577
+
1578
+ See Also
1579
+ --------
1580
+ scipy.integrate.fixed_quad
1581
+
1582
+ References
1583
+ ----------
1584
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
1585
+ Handbook of Mathematical Functions with Formulas,
1586
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
1587
+
1588
+ """
1589
+ m = int(n)
1590
+ if n < 1 or n != m:
1591
+ raise ValueError("n must be a positive integer.")
1592
+ if alpha < -0.5:
1593
+ raise ValueError("alpha must be greater than -0.5.")
1594
+ elif alpha == 0.0:
1595
+ # C(n,0,x) == 0 uniformly, however, as alpha->0, C(n,alpha,x)->T(n,x)
1596
+ # strictly, we should just error out here, since the roots are not
1597
+ # really defined, but we used to return something useful, so let's
1598
+ # keep doing so.
1599
+ return roots_chebyt(n, mu)
1600
+
1601
+ if alpha <= 170:
1602
+ mu0 = (np.sqrt(np.pi) * _ufuncs.gamma(alpha + 0.5)) \
1603
+ / _ufuncs.gamma(alpha + 1)
1604
+ else:
1605
+ # For large alpha we use a Taylor series expansion around inf,
1606
+ # expressed as a 6th order polynomial of a^-1 and using Horner's
1607
+ # method to minimize computation and maximize precision
1608
+ inv_alpha = 1. / alpha
1609
+ coeffs = np.array([0.000207186, -0.00152206, -0.000640869,
1610
+ 0.00488281, 0.0078125, -0.125, 1.])
1611
+ mu0 = coeffs[0]
1612
+ for term in range(1, len(coeffs)):
1613
+ mu0 = mu0 * inv_alpha + coeffs[term]
1614
+ mu0 = mu0 * np.sqrt(np.pi / alpha)
1615
+ def an_func(k):
1616
+ return 0.0 * k
1617
+ def bn_func(k):
1618
+ return np.sqrt(k * (k + 2 * alpha - 1) / (4 * (k + alpha) * (k + alpha - 1)))
1619
+ def f(n, x):
1620
+ return _ufuncs.eval_gegenbauer(n, alpha, x)
1621
+ def df(n, x):
1622
+ return (
1623
+ -n * x * _ufuncs.eval_gegenbauer(n, alpha, x)
1624
+ + (n + 2 * alpha - 1) * _ufuncs.eval_gegenbauer(n - 1, alpha, x)
1625
+ ) / (1 - x ** 2)
1626
+ return _gen_roots_and_weights(m, mu0, an_func, bn_func, f, df, True, mu)
1627
+
1628
+
1629
+ def gegenbauer(n, alpha, monic=False):
1630
+ r"""Gegenbauer (ultraspherical) polynomial.
1631
+
1632
+ Defined to be the solution of the second-order linear ordinary differential equation
1633
+
1634
+ .. math::
1635
+ (1 - x^2)\frac{d^2}{dx^2}C_n^{(\alpha)}(x)
1636
+ - (2\alpha + 1)x\frac{d}{dx}C_n^{(\alpha)}(x)
1637
+ + n(n + 2\alpha)C_n^{(\alpha)}(x) = 0
1638
+
1639
+ for :math:`\alpha > -1/2`; :math:`C_n^{(\alpha)}` is a polynomial
1640
+ of degree :math:`n`.
1641
+
1642
+ Parameters
1643
+ ----------
1644
+ n : int
1645
+ Degree of the polynomial.
1646
+ alpha : float
1647
+ Parameter, must be greater than -0.5.
1648
+ monic : bool, optional
1649
+ If `True`, scale the leading coefficient to be 1. Default is
1650
+ `False`.
1651
+
1652
+ Returns
1653
+ -------
1654
+ C : orthopoly1d
1655
+ Gegenbauer polynomial.
1656
+
1657
+ Notes
1658
+ -----
1659
+ The polynomials :math:`C_n^{(\alpha)}` are orthogonal on
1660
+ :math:`[-1,1]` with respect to the weight function :math:`(1 - x^2)^{(\alpha -
1661
+ 1/2)}`.
1662
+
1663
+ Examples
1664
+ --------
1665
+ >>> import numpy as np
1666
+ >>> from scipy import special
1667
+ >>> import matplotlib.pyplot as plt
1668
+
1669
+ We can initialize a variable ``p`` as a Gegenbauer polynomial using the
1670
+ `gegenbauer` function and evaluate at a point ``x = 1``.
1671
+
1672
+ >>> p = special.gegenbauer(3, 0.5, monic=False)
1673
+ >>> p
1674
+ poly1d([ 2.5, 0. , -1.5, 0. ])
1675
+ >>> p(1)
1676
+ 1.0
1677
+
1678
+ To evaluate ``p`` at various points ``x`` in the interval ``(-3, 3)``,
1679
+ simply pass an array ``x`` to ``p`` as follows:
1680
+
1681
+ >>> x = np.linspace(-3, 3, 400)
1682
+ >>> y = p(x)
1683
+
1684
+ We can then visualize ``x, y`` using `matplotlib.pyplot`.
1685
+
1686
+ >>> fig, ax = plt.subplots()
1687
+ >>> ax.plot(x, y)
1688
+ >>> ax.set_title("Gegenbauer (ultraspherical) polynomial of degree 3")
1689
+ >>> ax.set_xlabel("x")
1690
+ >>> ax.set_ylabel(r"$C_3^{(0.5)}(x)$")
1691
+ >>> plt.show()
1692
+
1693
+ """
1694
+ if not np.isfinite(alpha) or alpha <= -0.5 :
1695
+ raise ValueError("`alpha` must be a finite number greater than -1/2")
1696
+ base = jacobi(n, alpha - 0.5, alpha - 0.5, monic=monic)
1697
+ if monic or n == 0:
1698
+ return base
1699
+ # Abrahmowitz and Stegan 22.5.20
1700
+ factor = (_gam(2*alpha + n) * _gam(alpha + 0.5) /
1701
+ _gam(2*alpha) / _gam(alpha + 0.5 + n))
1702
+ base._scale(factor)
1703
+ base.__dict__['_eval_func'] = lambda x: _ufuncs.eval_gegenbauer(float(n),
1704
+ alpha, x)
1705
+ return base
1706
+
1707
+ # Chebyshev of the first kind: T_n(x) =
1708
+ # n! sqrt(pi) / _gam(n+1./2)* P^(-1/2,-1/2)_n(x)
1709
+ # Computed anew.
1710
+
1711
+
1712
+ def roots_chebyt(n, mu=False):
1713
+ r"""Gauss-Chebyshev (first kind) quadrature.
1714
+
1715
+ Computes the sample points and weights for Gauss-Chebyshev
1716
+ quadrature. The sample points are the roots of the nth degree
1717
+ Chebyshev polynomial of the first kind, :math:`T_n(x)`. These
1718
+ sample points and weights correctly integrate polynomials of
1719
+ degree :math:`2n - 1` or less over the interval :math:`[-1, 1]`
1720
+ with weight function :math:`w(x) = 1/\sqrt{1 - x^2}`. See 22.2.4
1721
+ in [AS]_ for more details.
1722
+
1723
+ Parameters
1724
+ ----------
1725
+ n : int
1726
+ quadrature order
1727
+ mu : bool, optional
1728
+ If True, return the sum of the weights, optional.
1729
+
1730
+ Returns
1731
+ -------
1732
+ x : ndarray
1733
+ Sample points
1734
+ w : ndarray
1735
+ Weights
1736
+ mu : float
1737
+ Sum of the weights
1738
+
1739
+ See Also
1740
+ --------
1741
+ scipy.integrate.fixed_quad
1742
+ numpy.polynomial.chebyshev.chebgauss
1743
+
1744
+ References
1745
+ ----------
1746
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
1747
+ Handbook of Mathematical Functions with Formulas,
1748
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
1749
+
1750
+ """
1751
+ m = int(n)
1752
+ if n < 1 or n != m:
1753
+ raise ValueError('n must be a positive integer.')
1754
+ x = _ufuncs._sinpi(np.arange(-m + 1, m, 2) / (2*m))
1755
+ w = np.full_like(x, pi/m)
1756
+ if mu:
1757
+ return x, w, pi
1758
+ else:
1759
+ return x, w
1760
+
1761
+
1762
+ def chebyt(n, monic=False):
1763
+ r"""Chebyshev polynomial of the first kind.
1764
+
1765
+ Defined to be the solution of
1766
+
1767
+ .. math::
1768
+ (1 - x^2)\frac{d^2}{dx^2}T_n(x) - x\frac{d}{dx}T_n(x)
1769
+ + n^2T_n(x) = 0;
1770
+
1771
+ :math:`T_n` is a polynomial of degree :math:`n`.
1772
+
1773
+ Parameters
1774
+ ----------
1775
+ n : int
1776
+ Degree of the polynomial.
1777
+ monic : bool, optional
1778
+ If `True`, scale the leading coefficient to be 1. Default is
1779
+ `False`.
1780
+
1781
+ Returns
1782
+ -------
1783
+ T : orthopoly1d
1784
+ Chebyshev polynomial of the first kind.
1785
+
1786
+ See Also
1787
+ --------
1788
+ chebyu : Chebyshev polynomial of the second kind.
1789
+
1790
+ Notes
1791
+ -----
1792
+ The polynomials :math:`T_n` are orthogonal over :math:`[-1, 1]`
1793
+ with weight function :math:`(1 - x^2)^{-1/2}`.
1794
+
1795
+ References
1796
+ ----------
1797
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
1798
+ Handbook of Mathematical Functions with Formulas,
1799
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
1800
+
1801
+ Examples
1802
+ --------
1803
+ Chebyshev polynomials of the first kind of order :math:`n` can
1804
+ be obtained as the determinant of specific :math:`n \times n`
1805
+ matrices. As an example we can check how the points obtained from
1806
+ the determinant of the following :math:`3 \times 3` matrix
1807
+ lay exactly on :math:`T_3`:
1808
+
1809
+ >>> import numpy as np
1810
+ >>> import matplotlib.pyplot as plt
1811
+ >>> from scipy.linalg import det
1812
+ >>> from scipy.special import chebyt
1813
+ >>> x = np.arange(-1.0, 1.0, 0.01)
1814
+ >>> fig, ax = plt.subplots()
1815
+ >>> ax.set_ylim(-2.0, 2.0)
1816
+ >>> ax.set_title(r'Chebyshev polynomial $T_3$')
1817
+ >>> ax.plot(x, chebyt(3)(x), label=rf'$T_3$')
1818
+ >>> for p in np.arange(-1.0, 1.0, 0.1):
1819
+ ... ax.plot(p,
1820
+ ... det(np.array([[p, 1, 0], [1, 2*p, 1], [0, 1, 2*p]])),
1821
+ ... 'rx')
1822
+ >>> plt.legend(loc='best')
1823
+ >>> plt.show()
1824
+
1825
+ They are also related to the Jacobi Polynomials
1826
+ :math:`P_n^{(-0.5, -0.5)}` through the relation:
1827
+
1828
+ .. math::
1829
+ P_n^{(-0.5, -0.5)}(x) = \frac{1}{4^n} \binom{2n}{n} T_n(x)
1830
+
1831
+ Let's verify it for :math:`n = 3`:
1832
+
1833
+ >>> from scipy.special import binom
1834
+ >>> from scipy.special import jacobi
1835
+ >>> x = np.arange(-1.0, 1.0, 0.01)
1836
+ >>> np.allclose(jacobi(3, -0.5, -0.5)(x),
1837
+ ... 1/64 * binom(6, 3) * chebyt(3)(x))
1838
+ True
1839
+
1840
+ We can plot the Chebyshev polynomials :math:`T_n` for some values
1841
+ of :math:`n`:
1842
+
1843
+ >>> x = np.arange(-1.5, 1.5, 0.01)
1844
+ >>> fig, ax = plt.subplots()
1845
+ >>> ax.set_ylim(-4.0, 4.0)
1846
+ >>> ax.set_title(r'Chebyshev polynomials $T_n$')
1847
+ >>> for n in np.arange(2,5):
1848
+ ... ax.plot(x, chebyt(n)(x), label=rf'$T_n={n}$')
1849
+ >>> plt.legend(loc='best')
1850
+ >>> plt.show()
1851
+
1852
+ """
1853
+ if n < 0:
1854
+ raise ValueError("n must be nonnegative.")
1855
+
1856
+ def wfunc(x):
1857
+ return 1.0 / sqrt(1 - x * x)
1858
+ if n == 0:
1859
+ return orthopoly1d([], [], pi, 1.0, wfunc, (-1, 1), monic,
1860
+ lambda x: _ufuncs.eval_chebyt(n, x))
1861
+ n1 = n
1862
+ x, w, mu = roots_chebyt(n1, mu=True)
1863
+ hn = pi / 2
1864
+ kn = 2**(n - 1)
1865
+ p = orthopoly1d(x, w, hn, kn, wfunc, (-1, 1), monic,
1866
+ lambda x: _ufuncs.eval_chebyt(n, x))
1867
+ return p
1868
+
1869
+ # Chebyshev of the second kind
1870
+ # U_n(x) = (n+1)! sqrt(pi) / (2*_gam(n+3./2)) * P^(1/2,1/2)_n(x)
1871
+
1872
+
1873
+ def roots_chebyu(n, mu=False):
1874
+ r"""Gauss-Chebyshev (second kind) quadrature.
1875
+
1876
+ Computes the sample points and weights for Gauss-Chebyshev
1877
+ quadrature. The sample points are the roots of the nth degree
1878
+ Chebyshev polynomial of the second kind, :math:`U_n(x)`. These
1879
+ sample points and weights correctly integrate polynomials of
1880
+ degree :math:`2n - 1` or less over the interval :math:`[-1, 1]`
1881
+ with weight function :math:`w(x) = \sqrt{1 - x^2}`. See 22.2.5 in
1882
+ [AS]_ for details.
1883
+
1884
+ Parameters
1885
+ ----------
1886
+ n : int
1887
+ quadrature order
1888
+ mu : bool, optional
1889
+ If True, return the sum of the weights, optional.
1890
+
1891
+ Returns
1892
+ -------
1893
+ x : ndarray
1894
+ Sample points
1895
+ w : ndarray
1896
+ Weights
1897
+ mu : float
1898
+ Sum of the weights
1899
+
1900
+ See Also
1901
+ --------
1902
+ scipy.integrate.fixed_quad
1903
+
1904
+ References
1905
+ ----------
1906
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
1907
+ Handbook of Mathematical Functions with Formulas,
1908
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
1909
+
1910
+ """
1911
+ m = int(n)
1912
+ if n < 1 or n != m:
1913
+ raise ValueError('n must be a positive integer.')
1914
+ t = np.arange(m, 0, -1) * pi / (m + 1)
1915
+ x = np.cos(t)
1916
+ w = pi * np.sin(t)**2 / (m + 1)
1917
+ if mu:
1918
+ return x, w, pi / 2
1919
+ else:
1920
+ return x, w
1921
+
1922
+
1923
+ def chebyu(n, monic=False):
1924
+ r"""Chebyshev polynomial of the second kind.
1925
+
1926
+ Defined to be the solution of
1927
+
1928
+ .. math::
1929
+ (1 - x^2)\frac{d^2}{dx^2}U_n(x) - 3x\frac{d}{dx}U_n(x)
1930
+ + n(n + 2)U_n(x) = 0;
1931
+
1932
+ :math:`U_n` is a polynomial of degree :math:`n`.
1933
+
1934
+ Parameters
1935
+ ----------
1936
+ n : int
1937
+ Degree of the polynomial.
1938
+ monic : bool, optional
1939
+ If `True`, scale the leading coefficient to be 1. Default is
1940
+ `False`.
1941
+
1942
+ Returns
1943
+ -------
1944
+ U : orthopoly1d
1945
+ Chebyshev polynomial of the second kind.
1946
+
1947
+ See Also
1948
+ --------
1949
+ chebyt : Chebyshev polynomial of the first kind.
1950
+
1951
+ Notes
1952
+ -----
1953
+ The polynomials :math:`U_n` are orthogonal over :math:`[-1, 1]`
1954
+ with weight function :math:`(1 - x^2)^{1/2}`.
1955
+
1956
+ References
1957
+ ----------
1958
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
1959
+ Handbook of Mathematical Functions with Formulas,
1960
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
1961
+
1962
+ Examples
1963
+ --------
1964
+ Chebyshev polynomials of the second kind of order :math:`n` can
1965
+ be obtained as the determinant of specific :math:`n \times n`
1966
+ matrices. As an example we can check how the points obtained from
1967
+ the determinant of the following :math:`3 \times 3` matrix
1968
+ lay exactly on :math:`U_3`:
1969
+
1970
+ >>> import numpy as np
1971
+ >>> import matplotlib.pyplot as plt
1972
+ >>> from scipy.linalg import det
1973
+ >>> from scipy.special import chebyu
1974
+ >>> x = np.arange(-1.0, 1.0, 0.01)
1975
+ >>> fig, ax = plt.subplots()
1976
+ >>> ax.set_ylim(-2.0, 2.0)
1977
+ >>> ax.set_title(r'Chebyshev polynomial $U_3$')
1978
+ >>> ax.plot(x, chebyu(3)(x), label=rf'$U_3$')
1979
+ >>> for p in np.arange(-1.0, 1.0, 0.1):
1980
+ ... ax.plot(p,
1981
+ ... det(np.array([[2*p, 1, 0], [1, 2*p, 1], [0, 1, 2*p]])),
1982
+ ... 'rx')
1983
+ >>> plt.legend(loc='best')
1984
+ >>> plt.show()
1985
+
1986
+ They satisfy the recurrence relation:
1987
+
1988
+ .. math::
1989
+ U_{2n-1}(x) = 2 T_n(x)U_{n-1}(x)
1990
+
1991
+ where :math:`T_n` is the Chebyshev polynomial of the first kind.
1992
+ Let's verify it for :math:`n = 2`:
1993
+
1994
+ >>> from scipy.special import chebyt
1995
+ >>> x = np.arange(-1.0, 1.0, 0.01)
1996
+ >>> np.allclose(chebyu(3)(x), 2 * chebyt(2)(x) * chebyu(1)(x))
1997
+ True
1998
+
1999
+ We can plot the Chebyshev polynomials :math:`U_n` for some values
2000
+ of :math:`n`:
2001
+
2002
+ >>> x = np.arange(-1.0, 1.0, 0.01)
2003
+ >>> fig, ax = plt.subplots()
2004
+ >>> ax.set_ylim(-1.5, 1.5)
2005
+ >>> ax.set_title(r'Chebyshev polynomials $U_n$')
2006
+ >>> for n in np.arange(1,5):
2007
+ ... ax.plot(x, chebyu(n)(x), label=rf'$U_n={n}$')
2008
+ >>> plt.legend(loc='best')
2009
+ >>> plt.show()
2010
+
2011
+ """
2012
+ base = jacobi(n, 0.5, 0.5, monic=monic)
2013
+ if monic:
2014
+ return base
2015
+ factor = sqrt(pi) / 2.0 * _gam(n + 2) / _gam(n + 1.5)
2016
+ base._scale(factor)
2017
+ return base
2018
+
2019
+ # Chebyshev of the first kind C_n(x)
2020
+
2021
+
2022
+ def roots_chebyc(n, mu=False):
2023
+ r"""Gauss-Chebyshev (first kind) quadrature.
2024
+
2025
+ Compute the sample points and weights for Gauss-Chebyshev
2026
+ quadrature. The sample points are the roots of the nth degree
2027
+ Chebyshev polynomial of the first kind, :math:`C_n(x)`. These
2028
+ sample points and weights correctly integrate polynomials of
2029
+ degree :math:`2n - 1` or less over the interval :math:`[-2, 2]`
2030
+ with weight function :math:`w(x) = 1 / \sqrt{1 - (x/2)^2}`. See
2031
+ 22.2.6 in [AS]_ for more details.
2032
+
2033
+ Parameters
2034
+ ----------
2035
+ n : int
2036
+ quadrature order
2037
+ mu : bool, optional
2038
+ If True, return the sum of the weights, optional.
2039
+
2040
+ Returns
2041
+ -------
2042
+ x : ndarray
2043
+ Sample points
2044
+ w : ndarray
2045
+ Weights
2046
+ mu : float
2047
+ Sum of the weights
2048
+
2049
+ See Also
2050
+ --------
2051
+ scipy.integrate.fixed_quad
2052
+
2053
+ References
2054
+ ----------
2055
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
2056
+ Handbook of Mathematical Functions with Formulas,
2057
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
2058
+
2059
+ """
2060
+ x, w, m = roots_chebyt(n, True)
2061
+ x *= 2
2062
+ w *= 2
2063
+ m *= 2
2064
+ if mu:
2065
+ return x, w, m
2066
+ else:
2067
+ return x, w
2068
+
2069
+
2070
+ def chebyc(n, monic=False):
2071
+ r"""Chebyshev polynomial of the first kind on :math:`[-2, 2]`.
2072
+
2073
+ Defined as :math:`C_n(x) = 2T_n(x/2)`, where :math:`T_n` is the
2074
+ nth Chebyshev polynomial of the first kind.
2075
+
2076
+ Parameters
2077
+ ----------
2078
+ n : int
2079
+ Degree of the polynomial.
2080
+ monic : bool, optional
2081
+ If `True`, scale the leading coefficient to be 1. Default is
2082
+ `False`.
2083
+
2084
+ Returns
2085
+ -------
2086
+ C : orthopoly1d
2087
+ Chebyshev polynomial of the first kind on :math:`[-2, 2]`.
2088
+
2089
+ See Also
2090
+ --------
2091
+ chebyt : Chebyshev polynomial of the first kind.
2092
+
2093
+ Notes
2094
+ -----
2095
+ The polynomials :math:`C_n(x)` are orthogonal over :math:`[-2, 2]`
2096
+ with weight function :math:`1/\sqrt{1 - (x/2)^2}`.
2097
+
2098
+ References
2099
+ ----------
2100
+ .. [1] Abramowitz and Stegun, "Handbook of Mathematical Functions"
2101
+ Section 22. National Bureau of Standards, 1972.
2102
+
2103
+ Examples
2104
+ --------
2105
+ Evaluate the Chebyshev polynomial :math:`C_3` at :math:`x = 1`:
2106
+
2107
+ >>> import numpy as np
2108
+ >>> from scipy.special import chebyc, chebyt
2109
+ >>> np.isclose(chebyc(3)(1), -2.0)
2110
+ True
2111
+
2112
+ The polynomial :math:`C_n` is a scaled Chebyshev polynomial of the
2113
+ first kind:
2114
+
2115
+ >>> x = np.linspace(-2, 2, 5)
2116
+ >>> np.allclose(chebyc(3)(x), 2 * chebyt(3)(x/2))
2117
+ True
2118
+
2119
+ Plot :math:`C_n` for several values of :math:`n`:
2120
+
2121
+ >>> import matplotlib.pyplot as plt
2122
+ >>> x = np.linspace(-2, 2, 400)
2123
+ >>> fig, ax = plt.subplots()
2124
+ >>> for n in range(4):
2125
+ ... ax.plot(x, chebyc(n)(x), label=rf"$C_{n}$")
2126
+ >>> ax.set_title(r"Chebyshev polynomials $C_n$")
2127
+ >>> ax.set_xlabel("x")
2128
+ >>> ax.legend(loc="best")
2129
+ >>> plt.show()
2130
+ """
2131
+ if n < 0:
2132
+ raise ValueError("n must be nonnegative.")
2133
+
2134
+ if n == 0:
2135
+ n1 = n + 1
2136
+ else:
2137
+ n1 = n
2138
+ x, w = roots_chebyc(n1)
2139
+ if n == 0:
2140
+ x, w = [], []
2141
+ hn = 4 * pi * ((n == 0) + 1)
2142
+ kn = 1.0
2143
+ p = orthopoly1d(x, w, hn, kn,
2144
+ wfunc=lambda x: 1.0 / sqrt(1 - x * x / 4.0),
2145
+ limits=(-2, 2), monic=monic)
2146
+ if not monic:
2147
+ p._scale(2.0 / p(2))
2148
+ p.__dict__['_eval_func'] = lambda x: _ufuncs.eval_chebyc(n, x)
2149
+ return p
2150
+
2151
+ # Chebyshev of the second kind S_n(x)
2152
+
2153
+
2154
+ def roots_chebys(n, mu=False):
2155
+ r"""Gauss-Chebyshev (second kind) quadrature.
2156
+
2157
+ Compute the sample points and weights for Gauss-Chebyshev
2158
+ quadrature. The sample points are the roots of the nth degree
2159
+ Chebyshev polynomial of the second kind, :math:`S_n(x)`. These
2160
+ sample points and weights correctly integrate polynomials of
2161
+ degree :math:`2n - 1` or less over the interval :math:`[-2, 2]`
2162
+ with weight function :math:`w(x) = \sqrt{1 - (x/2)^2}`. See 22.2.7
2163
+ in [AS]_ for more details.
2164
+
2165
+ Parameters
2166
+ ----------
2167
+ n : int
2168
+ quadrature order
2169
+ mu : bool, optional
2170
+ If True, return the sum of the weights, optional.
2171
+
2172
+ Returns
2173
+ -------
2174
+ x : ndarray
2175
+ Sample points
2176
+ w : ndarray
2177
+ Weights
2178
+ mu : float
2179
+ Sum of the weights
2180
+
2181
+ See Also
2182
+ --------
2183
+ scipy.integrate.fixed_quad
2184
+
2185
+ References
2186
+ ----------
2187
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
2188
+ Handbook of Mathematical Functions with Formulas,
2189
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
2190
+
2191
+ """
2192
+ x, w, m = roots_chebyu(n, True)
2193
+ x *= 2
2194
+ w *= 2
2195
+ m *= 2
2196
+ if mu:
2197
+ return x, w, m
2198
+ else:
2199
+ return x, w
2200
+
2201
+
2202
+ def chebys(n, monic=False):
2203
+ r"""Chebyshev polynomial of the second kind on :math:`[-2, 2]`.
2204
+
2205
+ Defined as :math:`S_n(x) = U_n(x/2)` where :math:`U_n` is the
2206
+ nth Chebyshev polynomial of the second kind.
2207
+
2208
+ Parameters
2209
+ ----------
2210
+ n : int
2211
+ Degree of the polynomial.
2212
+ monic : bool, optional
2213
+ If `True`, scale the leading coefficient to be 1. Default is
2214
+ `False`.
2215
+
2216
+ Returns
2217
+ -------
2218
+ S : orthopoly1d
2219
+ Chebyshev polynomial of the second kind on :math:`[-2, 2]`.
2220
+
2221
+ See Also
2222
+ --------
2223
+ chebyu : Chebyshev polynomial of the second kind
2224
+
2225
+ Notes
2226
+ -----
2227
+ The polynomials :math:`S_n(x)` are orthogonal over :math:`[-2, 2]`
2228
+ with weight function :math:`\sqrt{1 - (x/2)^2}`.
2229
+
2230
+ References
2231
+ ----------
2232
+ .. [1] Abramowitz and Stegun, "Handbook of Mathematical Functions"
2233
+ Section 22. National Bureau of Standards, 1972.
2234
+
2235
+ Examples
2236
+ --------
2237
+ Evaluate the Chebyshev polynomial of the second kind :math:`S_3` at :math:`x = 1`:
2238
+
2239
+ >>> import numpy as np
2240
+ >>> from scipy.special import chebys, chebyu
2241
+ >>> np.isclose(chebys(3)(1), -1.0)
2242
+ True
2243
+
2244
+ The polynomial :math:`S_n` is a scaled Chebyshev polynomial of the
2245
+ second kind:
2246
+
2247
+ >>> x = np.linspace(-2, 2, 5)
2248
+ >>> np.allclose(chebys(3)(x), chebyu(3)(x/2))
2249
+ True
2250
+
2251
+ Plot :math:`S_n` for several values of :math:`n`:
2252
+
2253
+ >>> import matplotlib.pyplot as plt
2254
+ >>> x = np.linspace(-2, 2, 400)
2255
+ >>> fig, ax = plt.subplots()
2256
+ >>> for n in range(4):
2257
+ ... ax.plot(x, chebys(n)(x), label=rf"$S_{n}$")
2258
+ >>> ax.set_title(r"Chebyshev polynomials $S_n$")
2259
+ >>> ax.set_xlabel("x")
2260
+ >>> ax.legend(loc="best")
2261
+ >>> plt.show()
2262
+ """
2263
+ if n < 0:
2264
+ raise ValueError("n must be nonnegative.")
2265
+
2266
+ if n == 0:
2267
+ n1 = n + 1
2268
+ else:
2269
+ n1 = n
2270
+ x, w = roots_chebys(n1)
2271
+ if n == 0:
2272
+ x, w = [], []
2273
+ hn = pi
2274
+ kn = 1.0
2275
+ p = orthopoly1d(x, w, hn, kn,
2276
+ wfunc=lambda x: sqrt(1 - x * x / 4.0),
2277
+ limits=(-2, 2), monic=monic)
2278
+ if not monic:
2279
+ factor = (n + 1.0) / p(2)
2280
+ p._scale(factor)
2281
+ p.__dict__['_eval_func'] = lambda x: _ufuncs.eval_chebys(n, x)
2282
+ return p
2283
+
2284
+ # Shifted Chebyshev of the first kind T^*_n(x)
2285
+
2286
+
2287
+ def roots_sh_chebyt(n, mu=False):
2288
+ r"""Gauss-Chebyshev (first kind, shifted) quadrature.
2289
+
2290
+ Compute the sample points and weights for Gauss-Chebyshev
2291
+ quadrature. The sample points are the roots of the nth degree
2292
+ shifted Chebyshev polynomial of the first kind, :math:`T_n(x)`.
2293
+ These sample points and weights correctly integrate polynomials of
2294
+ degree :math:`2n - 1` or less over the interval :math:`[0, 1]`
2295
+ with weight function :math:`w(x) = 1/\sqrt{x - x^2}`. See 22.2.8
2296
+ in [AS]_ for more details.
2297
+
2298
+ Parameters
2299
+ ----------
2300
+ n : int
2301
+ quadrature order.
2302
+ mu : bool, optional
2303
+ If True, return the sum of the weights, optional.
2304
+
2305
+ Returns
2306
+ -------
2307
+ x : ndarray
2308
+ Sample points.
2309
+ w : ndarray
2310
+ Weights.
2311
+ mu : float
2312
+ Sum of the weights.
2313
+
2314
+ See Also
2315
+ --------
2316
+ scipy.integrate.fixed_quad
2317
+
2318
+ References
2319
+ ----------
2320
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
2321
+ Handbook of Mathematical Functions with Formulas,
2322
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
2323
+
2324
+ """
2325
+ xw = roots_chebyt(n, mu)
2326
+ return ((xw[0] + 1) / 2,) + xw[1:]
2327
+
2328
+
2329
+ def sh_chebyt(n, monic=False):
2330
+ r"""Shifted Chebyshev polynomial of the first kind.
2331
+
2332
+ Defined as :math:`T^*_n(x) = T_n(2x - 1)` for :math:`T_n` the nth
2333
+ Chebyshev polynomial of the first kind.
2334
+
2335
+ Parameters
2336
+ ----------
2337
+ n : int
2338
+ Degree of the polynomial.
2339
+ monic : bool, optional
2340
+ If `True`, scale the leading coefficient to be 1. Default is
2341
+ `False`.
2342
+
2343
+ Returns
2344
+ -------
2345
+ T : orthopoly1d
2346
+ Shifted Chebyshev polynomial of the first kind.
2347
+
2348
+ Notes
2349
+ -----
2350
+ The polynomials :math:`T^*_n` are orthogonal over :math:`[0, 1]`
2351
+ with weight function :math:`(x - x^2)^{-1/2}`.
2352
+
2353
+ Examples
2354
+ --------
2355
+ Evaluate the shifted Chebyshev polynomial of the first kind :math:`T^*_3` at
2356
+ :math:`x = 0.75`:
2357
+
2358
+ >>> import numpy as np
2359
+ >>> from scipy.special import chebyt, sh_chebyt
2360
+ >>> np.isclose(sh_chebyt(3)(0.75), -1.0)
2361
+ True
2362
+
2363
+ The polynomial :math:`T^*_n` is the Chebyshev polynomial
2364
+ :math:`T_n` shifted from :math:`[-1, 1]` to :math:`[0, 1]`:
2365
+
2366
+ >>> x = np.linspace(0, 1, 5)
2367
+ >>> np.allclose(sh_chebyt(3)(x), chebyt(3)(2*x - 1))
2368
+ True
2369
+
2370
+ Plot :math:`T^*_n` for several values of :math:`n`:
2371
+
2372
+ >>> import matplotlib.pyplot as plt
2373
+ >>> x = np.linspace(0, 1, 400)
2374
+ >>> fig, ax = plt.subplots()
2375
+ >>> for n in range(4):
2376
+ ... ax.plot(x, sh_chebyt(n)(x), label=rf"$T^*_{n}$")
2377
+ >>> ax.set_title(r"Shifted Chebyshev polynomials $T^*_n$")
2378
+ >>> ax.set_xlabel("x")
2379
+ >>> ax.legend(loc="best")
2380
+ >>> plt.show()
2381
+
2382
+ """
2383
+ base = sh_jacobi(n, 0.0, 0.5, monic=monic)
2384
+ if monic:
2385
+ return base
2386
+ if n > 0:
2387
+ factor = 4**n / 2.0
2388
+ else:
2389
+ factor = 1.0
2390
+ base._scale(factor)
2391
+ return base
2392
+
2393
+
2394
+ # Shifted Chebyshev of the second kind U^*_n(x)
2395
+ def roots_sh_chebyu(n, mu=False):
2396
+ r"""Gauss-Chebyshev (second kind, shifted) quadrature.
2397
+
2398
+ Computes the sample points and weights for Gauss-Chebyshev
2399
+ quadrature. The sample points are the roots of the nth degree
2400
+ shifted Chebyshev polynomial of the second kind, :math:`U_n(x)`.
2401
+ These sample points and weights correctly integrate polynomials of
2402
+ degree :math:`2n - 1` or less over the interval :math:`[0, 1]`
2403
+ with weight function :math:`w(x) = \sqrt{x - x^2}`. See 22.2.9 in
2404
+ [AS]_ for more details.
2405
+
2406
+ Parameters
2407
+ ----------
2408
+ n : int
2409
+ quadrature order
2410
+ mu : bool, optional
2411
+ If True, return the sum of the weights, optional.
2412
+
2413
+ Returns
2414
+ -------
2415
+ x : ndarray
2416
+ Sample points
2417
+ w : ndarray
2418
+ Weights
2419
+ mu : float
2420
+ Sum of the weights
2421
+
2422
+ See Also
2423
+ --------
2424
+ scipy.integrate.fixed_quad
2425
+
2426
+ References
2427
+ ----------
2428
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
2429
+ Handbook of Mathematical Functions with Formulas,
2430
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
2431
+
2432
+ """
2433
+ x, w, m = roots_chebyu(n, True)
2434
+ x = (x + 1) / 2
2435
+ m_us = _ufuncs.beta(1.5, 1.5)
2436
+ w *= m_us / m
2437
+ if mu:
2438
+ return x, w, m_us
2439
+ else:
2440
+ return x, w
2441
+
2442
+
2443
+ def sh_chebyu(n, monic=False):
2444
+ r"""Shifted Chebyshev polynomial of the second kind.
2445
+
2446
+ Defined as :math:`U^*_n(x) = U_n(2x - 1)` for :math:`U_n` the nth
2447
+ Chebyshev polynomial of the second kind.
2448
+
2449
+ Parameters
2450
+ ----------
2451
+ n : int
2452
+ Degree of the polynomial.
2453
+ monic : bool, optional
2454
+ If `True`, scale the leading coefficient to be 1. Default is
2455
+ `False`.
2456
+
2457
+ Returns
2458
+ -------
2459
+ U : orthopoly1d
2460
+ Shifted Chebyshev polynomial of the second kind.
2461
+
2462
+ Notes
2463
+ -----
2464
+ The polynomials :math:`U^*_n` are orthogonal over :math:`[0, 1]`
2465
+ with weight function :math:`(x - x^2)^{1/2}`.
2466
+
2467
+ Examples
2468
+ --------
2469
+ Evaluate the shifted Chebyshev polynomial of the second kind :math:`U^*_3` at
2470
+ :math:`x = 0.75`:
2471
+
2472
+ >>> import numpy as np
2473
+ >>> from scipy.special import chebyu, sh_chebyu
2474
+ >>> np.isclose(sh_chebyu(3)(0.75), -1.0)
2475
+ True
2476
+
2477
+ The polynomial :math:`U^*_n` is the Chebyshev polynomial
2478
+ :math:`U_n` shifted from :math:`[-1, 1]` to :math:`[0, 1]`:
2479
+
2480
+ >>> x = np.linspace(0, 1, 5)
2481
+ >>> np.allclose(sh_chebyu(3)(x), chebyu(3)(2*x - 1))
2482
+ True
2483
+
2484
+ Plot :math:`U^*_n` for several values of :math:`n`:
2485
+
2486
+ >>> import matplotlib.pyplot as plt
2487
+ >>> x = np.linspace(0, 1, 400)
2488
+ >>> fig, ax = plt.subplots()
2489
+ >>> for n in range(4):
2490
+ ... ax.plot(x, sh_chebyu(n)(x), label=rf"$U^*_{n}$")
2491
+ >>> ax.set_title(r"Shifted Chebyshev polynomials $U^*_n$")
2492
+ >>> ax.set_xlabel("x")
2493
+ >>> ax.legend(loc="best")
2494
+ >>> plt.show()
2495
+
2496
+ """
2497
+ base = sh_jacobi(n, 2.0, 1.5, monic=monic)
2498
+ if monic:
2499
+ return base
2500
+ factor = 4**n
2501
+ base._scale(factor)
2502
+ return base
2503
+
2504
+ # Legendre
2505
+
2506
+
2507
+ def roots_legendre(n, mu=False):
2508
+ r"""Gauss-Legendre quadrature.
2509
+
2510
+ Compute the sample points and weights for Gauss-Legendre
2511
+ quadrature [GL]_. The sample points are the roots of the nth degree
2512
+ Legendre polynomial :math:`P_n(x)`. These sample points and
2513
+ weights correctly integrate polynomials of degree :math:`2n - 1`
2514
+ or less over the interval :math:`[-1, 1]` with weight function
2515
+ :math:`w(x) = 1`. See 2.2.10 in [AS]_ for more details.
2516
+
2517
+ Parameters
2518
+ ----------
2519
+ n : int
2520
+ quadrature order.
2521
+ mu : bool, optional
2522
+ If True, return the sum of the weights, optional.
2523
+
2524
+ Returns
2525
+ -------
2526
+ x : ndarray
2527
+ Sample points.
2528
+ w : ndarray
2529
+ Weights.
2530
+ mu : float
2531
+ Sum of the weights.
2532
+
2533
+ See Also
2534
+ --------
2535
+ scipy.integrate.fixed_quad
2536
+ numpy.polynomial.legendre.leggauss
2537
+
2538
+ References
2539
+ ----------
2540
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
2541
+ Handbook of Mathematical Functions with Formulas,
2542
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
2543
+ .. [GL] Gauss-Legendre quadrature, Wikipedia,
2544
+ https://en.wikipedia.org/wiki/Gauss%E2%80%93Legendre_quadrature
2545
+
2546
+ Examples
2547
+ --------
2548
+ >>> import numpy as np
2549
+ >>> from scipy.special import roots_legendre, eval_legendre
2550
+ >>> roots, weights = roots_legendre(9)
2551
+
2552
+ ``roots`` holds the roots, and ``weights`` holds the weights for
2553
+ Gauss-Legendre quadrature.
2554
+
2555
+ >>> roots
2556
+ array([-0.96816024, -0.83603111, -0.61337143, -0.32425342, 0. ,
2557
+ 0.32425342, 0.61337143, 0.83603111, 0.96816024])
2558
+ >>> weights
2559
+ array([0.08127439, 0.18064816, 0.2606107 , 0.31234708, 0.33023936,
2560
+ 0.31234708, 0.2606107 , 0.18064816, 0.08127439])
2561
+
2562
+ Verify that we have the roots by evaluating the degree 9 Legendre
2563
+ polynomial at ``roots``. All the values are approximately zero:
2564
+
2565
+ >>> eval_legendre(9, roots)
2566
+ array([-8.88178420e-16, -2.22044605e-16, 1.11022302e-16, 1.11022302e-16,
2567
+ 0.00000000e+00, -5.55111512e-17, -1.94289029e-16, 1.38777878e-16,
2568
+ -8.32667268e-17])
2569
+
2570
+ Here we'll show how the above values can be used to estimate the
2571
+ integral from 1 to 2 of f(t) = t + 1/t with Gauss-Legendre
2572
+ quadrature [GL]_. First define the function and the integration
2573
+ limits.
2574
+
2575
+ >>> def f(t):
2576
+ ... return t + 1/t
2577
+ ...
2578
+ >>> a = 1
2579
+ >>> b = 2
2580
+
2581
+ We'll use ``integral(f(t), t=a, t=b)`` to denote the definite integral
2582
+ of f from t=a to t=b. The sample points in ``roots`` are from the
2583
+ interval [-1, 1], so we'll rewrite the integral with the simple change
2584
+ of variable::
2585
+
2586
+ x = 2/(b - a) * t - (a + b)/(b - a)
2587
+
2588
+ with inverse::
2589
+
2590
+ t = (b - a)/2 * x + (a + b)/2
2591
+
2592
+ Then::
2593
+
2594
+ integral(f(t), a, b) =
2595
+ (b - a)/2 * integral(f((b-a)/2*x + (a+b)/2), x=-1, x=1)
2596
+
2597
+ We can approximate the latter integral with the values returned
2598
+ by `roots_legendre`.
2599
+
2600
+ Map the roots computed above from [-1, 1] to [a, b].
2601
+
2602
+ >>> t = (b - a)/2 * roots + (a + b)/2
2603
+
2604
+ Approximate the integral as the weighted sum of the function values.
2605
+
2606
+ >>> (b - a)/2 * f(t).dot(weights)
2607
+ 2.1931471805599276
2608
+
2609
+ Compare that to the exact result, which is 3/2 + log(2):
2610
+
2611
+ >>> 1.5 + np.log(2)
2612
+ 2.1931471805599454
2613
+
2614
+ """
2615
+ m = int(n)
2616
+ if n < 1 or n != m:
2617
+ raise ValueError("n must be a positive integer.")
2618
+
2619
+ mu0 = 2.0
2620
+ def an_func(k):
2621
+ return 0.0 * k
2622
+ def bn_func(k):
2623
+ return k * np.sqrt(1.0 / (4 * k * k - 1))
2624
+ f = _ufuncs.eval_legendre
2625
+ def df(n, x):
2626
+ return (-n * x * _ufuncs.eval_legendre(n, x)
2627
+ + n * _ufuncs.eval_legendre(n - 1, x)) / (1 - x ** 2)
2628
+ return _gen_roots_and_weights(m, mu0, an_func, bn_func, f, df, True, mu)
2629
+
2630
+
2631
+ def legendre(n, monic=False):
2632
+ r"""Legendre polynomial.
2633
+
2634
+ Defined to be the solution of
2635
+
2636
+ .. math::
2637
+ \frac{d}{dx}\left[(1 - x^2)\frac{d}{dx}P_n(x)\right]
2638
+ + n(n + 1)P_n(x) = 0;
2639
+
2640
+ :math:`P_n(x)` is a polynomial of degree :math:`n`.
2641
+
2642
+ Parameters
2643
+ ----------
2644
+ n : int
2645
+ Degree of the polynomial.
2646
+ monic : bool, optional
2647
+ If `True`, scale the leading coefficient to be 1. Default is
2648
+ `False`.
2649
+
2650
+ Returns
2651
+ -------
2652
+ P : orthopoly1d
2653
+ Legendre polynomial.
2654
+
2655
+ Notes
2656
+ -----
2657
+ The polynomials :math:`P_n` are orthogonal over :math:`[-1, 1]`
2658
+ with weight function 1.
2659
+
2660
+ Examples
2661
+ --------
2662
+ Generate the 3rd-order Legendre polynomial 1/2*(5x^3 + 0x^2 - 3x + 0):
2663
+
2664
+ >>> from scipy.special import legendre
2665
+ >>> legendre(3)
2666
+ poly1d([ 2.5, 0. , -1.5, 0. ])
2667
+
2668
+ """
2669
+ if n < 0:
2670
+ raise ValueError("n must be nonnegative.")
2671
+
2672
+ if n == 0:
2673
+ n1 = n + 1
2674
+ else:
2675
+ n1 = n
2676
+ x, w = roots_legendre(n1)
2677
+ if n == 0:
2678
+ x, w = [], []
2679
+ hn = 2.0 / (2 * n + 1)
2680
+ kn = _gam(2 * n + 1) / _gam(n + 1)**2 / 2.0**n
2681
+ p = orthopoly1d(x, w, hn, kn, wfunc=lambda x: 1.0, limits=(-1, 1),
2682
+ monic=monic,
2683
+ eval_func=lambda x: _ufuncs.eval_legendre(n, x))
2684
+ return p
2685
+
2686
+ # Shifted Legendre P^*_n(x)
2687
+
2688
+
2689
+ def roots_sh_legendre(n, mu=False):
2690
+ r"""Gauss-Legendre (shifted) quadrature.
2691
+
2692
+ Compute the sample points and weights for Gauss-Legendre
2693
+ quadrature. The sample points are the roots of the nth degree
2694
+ shifted Legendre polynomial :math:`P^*_n(x)`. These sample points
2695
+ and weights correctly integrate polynomials of degree :math:`2n -
2696
+ 1` or less over the interval :math:`[0, 1]` with weight function
2697
+ :math:`w(x) = 1.0`. See 2.2.11 in [AS]_ for details.
2698
+
2699
+ Parameters
2700
+ ----------
2701
+ n : int
2702
+ quadrature order
2703
+ mu : bool, optional
2704
+ If True, return the sum of the weights, optional.
2705
+
2706
+ Returns
2707
+ -------
2708
+ x : ndarray
2709
+ Sample points
2710
+ w : ndarray
2711
+ Weights
2712
+ mu : float
2713
+ Sum of the weights
2714
+
2715
+ See Also
2716
+ --------
2717
+ scipy.integrate.fixed_quad
2718
+
2719
+ References
2720
+ ----------
2721
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
2722
+ Handbook of Mathematical Functions with Formulas,
2723
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
2724
+
2725
+ Examples
2726
+ --------
2727
+
2728
+ Compute nodes and weights for a 7th-order shifted Gauss-Legendre quadrature:
2729
+
2730
+ >>> from scipy.special import roots_sh_legendre, eval_sh_legendre
2731
+ >>> x, w = roots_sh_legendre(7)
2732
+ >>> x
2733
+ array([0.02544604, 0.12923441, 0.29707742, 0.5, 0.70292258, 0.87076559,
2734
+ 0.97455396])
2735
+ >>> w
2736
+ array([0.06474248, 0.1398527, 0.19091503, 0.20897959, 0.19091503, 0.1398527,
2737
+ 0.06474248])
2738
+
2739
+ Verify that ``x`` are the roots of the degree-7 shifted Legendre polynomial:
2740
+
2741
+ >>> eval_sh_legendre(7, x)
2742
+ array([5.55111512e-16, 1.11022302e-16, 3.33066907e-16, 0.00000000e+00,
2743
+ -2.22044605e-16, -1.11022302e-16, -1.85962357e-15])
2744
+
2745
+ The sum of the weights for shifted Gauss-Legendre quadrature is always 1:
2746
+
2747
+ >>> x, w, mu = roots_sh_legendre(10, mu=True)
2748
+ >>> mu
2749
+ 1.0 # Sum of weights of shifted Gauss-Legendre quadrature is always 1
2750
+
2751
+ """
2752
+ x, w = roots_legendre(n)
2753
+ x = (x + 1) / 2
2754
+ w /= 2
2755
+ if mu:
2756
+ return x, w, 1.0
2757
+ else:
2758
+ return x, w
2759
+
2760
+
2761
+ def sh_legendre(n, monic=False):
2762
+ r"""
2763
+ Shifted Legendre polynomial.
2764
+
2765
+ Defined as :math:`P^*_n(x) = P_n(2x - 1)` for :math:`P_n` the nth
2766
+ Legendre polynomial.
2767
+
2768
+ Parameters
2769
+ ----------
2770
+ n : int
2771
+ Degree of the polynomial.
2772
+ monic : bool, optional
2773
+ If `True`, scale the leading coefficient to be 1. Default is
2774
+ `False`.
2775
+
2776
+ Returns
2777
+ -------
2778
+ P : orthopoly1d
2779
+ Shifted Legendre polynomial.
2780
+
2781
+ See Also
2782
+ --------
2783
+ scipy.special.legendre
2784
+ scipy.special.roots_sh_legendre
2785
+
2786
+ Notes
2787
+ -----
2788
+ The polynomials :math:`P^*_n` are orthogonal over :math:`[0, 1]`
2789
+ with weight function 1.
2790
+
2791
+ Examples
2792
+ --------
2793
+ The shifted Legendre polynomials :math:`P_n^*` are related
2794
+ to the non-shifted polynomials :math:`P_n` by
2795
+ :math:`P_n^*(x) = P_n(2x - 1)`. We can verify this on the
2796
+ interval :math:`[0, 1]`:
2797
+
2798
+ >>> import numpy as np
2799
+ >>> from scipy.special import sh_legendre, legendre
2800
+ >>> from scipy.integrate import trapezoid
2801
+ >>> x = np.arange(0.0, 1.0, 0.01)
2802
+ >>> n = 3
2803
+ >>> np.allclose(sh_legendre(n)(x), legendre(n)(2*x - 1))
2804
+ True
2805
+
2806
+ The polynomials :math:`P_n^*` satisfy a recurrence
2807
+ relation obtained by the change of variables
2808
+ :math:`t = 2x - 1` in the standard Legendre recurrence:
2809
+
2810
+ .. math::
2811
+
2812
+ (n+1) P_{n+1}^*(x) = (2n+1)(2x-1)\,P_n^*(x) - n\,P_{n-1}^*(x).
2813
+
2814
+ This can be easily checked on :math:`[0, 1]`
2815
+ for :math:`n = 3`:
2816
+
2817
+ >>> n = 3
2818
+ >>> x = np.linspace(0.0, 1.0, 101)
2819
+ >>> lhs = (n + 1) * sh_legendre(n + 1)(x)
2820
+ >>> rhs = (
2821
+ ... (2*n + 1) * (2*x - 1) * sh_legendre(n)(x)
2822
+ ... - n * sh_legendre(n - 1)(x)
2823
+ ... )
2824
+ >>> np.allclose(lhs, rhs)
2825
+ True
2826
+
2827
+ Orthogonality over :math:`[0,1]` with weight 1 can be
2828
+ checked numerically; for example, :math:`P_2^*`
2829
+ is orthogonal to :math:`P_3^*`:
2830
+
2831
+ >>> x = np.linspace(0.0, 1.0, 400)
2832
+ >>> y = sh_legendre(2)(x) * sh_legendre(3)(x)
2833
+ >>> np.isclose(trapezoid(y, x), 0.0, atol=1e-12)
2834
+ True
2835
+ """
2836
+ if n < 0:
2837
+ raise ValueError("n must be nonnegative.")
2838
+
2839
+ def wfunc(x):
2840
+ return 0.0 * x + 1.0
2841
+ if n == 0:
2842
+ return orthopoly1d([], [], 1.0, 1.0, wfunc, (0, 1), monic,
2843
+ lambda x: _ufuncs.eval_sh_legendre(n, x))
2844
+ x, w = roots_sh_legendre(n)
2845
+ hn = 1.0 / (2 * n + 1.0)
2846
+ kn = _gam(2 * n + 1) / _gam(n + 1)**2
2847
+ p = orthopoly1d(x, w, hn, kn, wfunc, limits=(0, 1), monic=monic,
2848
+ eval_func=lambda x: _ufuncs.eval_sh_legendre(n, x))
2849
+ return p
2850
+
2851
+
2852
+ # Make the old root function names an alias for the new ones
2853
+ _modattrs = globals()
2854
+ for newfun, oldfun in _rootfuns_map.items():
2855
+ _modattrs[oldfun] = _modattrs[newfun]
2856
+ __all__.append(oldfun)
v12x/pylib/scipy/special/_precompute/__init__.py ADDED
File without changes
v12x/pylib/scipy/special/_precompute/cosine_cdf.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mpmath
2
+
3
+
4
+ def f(x):
5
+ return (mpmath.pi + x + mpmath.sin(x)) / (2*mpmath.pi)
6
+
7
+
8
+ # Note: 40 digits might be overkill; a few more digits than the default
9
+ # might be sufficient.
10
+ mpmath.mp.dps = 40
11
+ ts = mpmath.taylor(f, -mpmath.pi, 20)
12
+ p, q = mpmath.pade(ts, 9, 10)
13
+
14
+ p = [float(c) for c in p]
15
+ q = [float(c) for c in q]
16
+ print('p =', p)
17
+ print('q =', q)
v12x/pylib/scipy/special/_precompute/expn_asy.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Precompute the polynomials for the asymptotic expansion of the
2
+ generalized exponential integral.
3
+
4
+ Sources
5
+ -------
6
+ [1] NIST, Digital Library of Mathematical Functions,
7
+ https://dlmf.nist.gov/8.20#ii
8
+
9
+ """
10
+ import os
11
+
12
+ try:
13
+ import sympy
14
+ from sympy import Poly
15
+ x = sympy.symbols('x')
16
+ except ImportError:
17
+ pass
18
+
19
+
20
+ def generate_A(K):
21
+ A = [Poly(1, x)]
22
+ for k in range(K):
23
+ A.append(Poly(1 - 2*k*x, x)*A[k] + Poly(x*(x + 1))*A[k].diff())
24
+ return A
25
+
26
+
27
+ WARNING = """\
28
+ /* This file was automatically generated by _precompute/expn_asy.py.
29
+ * Do not edit it manually!
30
+ */
31
+ """
32
+
33
+
34
+ def main():
35
+ print(__doc__)
36
+ fn = os.path.join('..', 'cephes', 'expn.h')
37
+
38
+ K = 12
39
+ A = generate_A(K)
40
+ with open(fn + '.new', 'w') as f:
41
+ f.write(WARNING)
42
+ f.write(f"#define nA {len(A)}\n")
43
+ for k, Ak in enumerate(A):
44
+ ', '.join([str(x.evalf(18)) for x in Ak.coeffs()])
45
+ f.write(f"static const double A{k}[] = {{tmp}};\n")
46
+ ", ".join([f"A{k}" for k in range(K + 1)])
47
+ f.write("static const double *A[] = {{tmp}};\n")
48
+ ", ".join([str(Ak.degree()) for Ak in A])
49
+ f.write("static const int Adegs[] = {{tmp}};\n")
50
+ os.rename(fn + '.new', fn)
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
v12x/pylib/scipy/special/_precompute/gammainc_asy.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Precompute coefficients of Temme's asymptotic expansion for gammainc.
3
+
4
+ This takes about 8 hours to run on a 2.3 GHz Macbook Pro with 4GB ram.
5
+
6
+ Sources:
7
+ [1] NIST, "Digital Library of Mathematical Functions",
8
+ https://dlmf.nist.gov/
9
+
10
+ """
11
+ import os
12
+ from scipy.special._precompute.utils import lagrange_inversion
13
+
14
+ try:
15
+ import mpmath as mp
16
+ except ImportError:
17
+ pass
18
+
19
+
20
+ def compute_a(n):
21
+ """a_k from DLMF 5.11.6"""
22
+ a = [mp.sqrt(2)/2]
23
+ for k in range(1, n):
24
+ ak = a[-1]/k
25
+ for j in range(1, len(a)):
26
+ ak -= a[j]*a[-j]/(j + 1)
27
+ ak /= a[0]*(1 + mp.mpf(1)/(k + 1))
28
+ a.append(ak)
29
+ return a
30
+
31
+
32
+ def compute_g(n):
33
+ """g_k from DLMF 5.11.3/5.11.5"""
34
+ a = compute_a(2*n)
35
+ g = [mp.sqrt(2)*mp.rf(0.5, k)*a[2*k] for k in range(n)]
36
+ return g
37
+
38
+
39
+ def eta(lam):
40
+ """Function from DLMF 8.12.1 shifted to be centered at 0."""
41
+ if lam > 0:
42
+ return mp.sqrt(2*(lam - mp.log(lam + 1)))
43
+ elif lam < 0:
44
+ return -mp.sqrt(2*(lam - mp.log(lam + 1)))
45
+ else:
46
+ return 0
47
+
48
+
49
+ def compute_alpha(n):
50
+ """alpha_n from DLMF 8.12.13"""
51
+ coeffs = mp.taylor(eta, 0, n - 1)
52
+ return lagrange_inversion(coeffs)
53
+
54
+
55
+ def compute_d(K, N):
56
+ """d_{k, n} from DLMF 8.12.12"""
57
+ M = N + 2*K
58
+ d0 = [-mp.mpf(1)/3]
59
+ alpha = compute_alpha(M + 2)
60
+ for n in range(1, M):
61
+ d0.append((n + 2)*alpha[n+2])
62
+ d = [d0]
63
+ g = compute_g(K)
64
+ for k in range(1, K):
65
+ dk = []
66
+ for n in range(M - 2*k):
67
+ dk.append((-1)**k*g[k]*d[0][n] + (n + 2)*d[k-1][n+2])
68
+ d.append(dk)
69
+ for k in range(K):
70
+ d[k] = d[k][:N]
71
+ return d
72
+
73
+
74
+ header = \
75
+ r"""/* This file was automatically generated by _precomp/gammainc.py.
76
+ * Do not edit it manually!
77
+ */
78
+
79
+ #ifndef IGAM_H
80
+ #define IGAM_H
81
+
82
+ #define K {}
83
+ #define N {}
84
+
85
+ static const double d[K][N] =
86
+ {{"""
87
+
88
+ footer = \
89
+ r"""
90
+ #endif
91
+ """
92
+
93
+
94
+ def main():
95
+ print(__doc__)
96
+ K = 25
97
+ N = 25
98
+ with mp.workdps(50):
99
+ d = compute_d(K, N)
100
+ fn = os.path.join(os.path.dirname(__file__), '..', 'cephes', 'igam.h')
101
+ with open(fn + '.new', 'w') as f:
102
+ f.write(header.format(K, N))
103
+ for k, row in enumerate(d):
104
+ row = [mp.nstr(x, 17, min_fixed=0, max_fixed=0) for x in row]
105
+ f.write('{')
106
+ f.write(", ".join(row))
107
+ if k < K - 1:
108
+ f.write('},\n')
109
+ else:
110
+ f.write('}};\n')
111
+ f.write(footer)
112
+ os.rename(fn + '.new', fn)
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
v12x/pylib/scipy/special/_precompute/gammainc_data.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compute gammainc and gammaincc for large arguments and parameters
2
+ and save the values to data files for use in tests. We can't just
3
+ compare to mpmath's gammainc in test_mpmath.TestSystematic because it
4
+ would take too long.
5
+
6
+ Note that mpmath's gammainc is computed using hypercomb, but since it
7
+ doesn't allow the user to increase the maximum number of terms used in
8
+ the series it doesn't converge for many arguments. To get around this
9
+ we copy the mpmath implementation but use more terms.
10
+
11
+ This takes about 17 minutes to run on a 2.3 GHz Macbook Pro with 4GB
12
+ ram.
13
+
14
+ Sources:
15
+ [1] Fredrik Johansson and others. mpmath: a Python library for
16
+ arbitrary-precision floating-point arithmetic (version 0.19),
17
+ December 2013. http://mpmath.org/.
18
+
19
+ """
20
+ import os
21
+ from time import time
22
+ import numpy as np
23
+ from numpy import pi
24
+
25
+ from scipy.special._mptestutils import mpf2float
26
+
27
+ try:
28
+ import mpmath as mp
29
+ except ImportError:
30
+ pass
31
+
32
+
33
+ def gammainc(a, x, dps=50, maxterms=10**8):
34
+ """Compute gammainc exactly like mpmath does but allow for more
35
+ summands in hypercomb. See
36
+
37
+ mpmath/functions/expintegrals.py#L134
38
+
39
+ in the mpmath GitHub repository.
40
+
41
+ """
42
+ with mp.workdps(dps):
43
+ z, a, b = mp.mpf(a), mp.mpf(x), mp.mpf(x)
44
+ G = [z]
45
+ negb = mp.fneg(b, exact=True)
46
+
47
+ def h(z):
48
+ T1 = [mp.exp(negb), b, z], [1, z, -1], [], G, [1], [1+z], b
49
+ return (T1,)
50
+
51
+ res = mp.hypercomb(h, [z], maxterms=maxterms)
52
+ return mpf2float(res)
53
+
54
+
55
+ def gammaincc(a, x, dps=50, maxterms=10**8):
56
+ """Compute gammaincc exactly like mpmath does but allow for more
57
+ terms in hypercomb. See
58
+
59
+ mpmath/functions/expintegrals.py#L187
60
+
61
+ in the mpmath GitHub repository.
62
+
63
+ """
64
+ with mp.workdps(dps):
65
+ z, a = a, x
66
+
67
+ if mp.isint(z):
68
+ try:
69
+ # mpmath has a fast integer path
70
+ return mpf2float(mp.gammainc(z, a=a, regularized=True))
71
+ except mp.libmp.NoConvergence:
72
+ pass
73
+ nega = mp.fneg(a, exact=True)
74
+ G = [z]
75
+ # Use 2F0 series when possible; fall back to lower gamma representation
76
+ try:
77
+ def h(z):
78
+ r = z-1
79
+ return [([mp.exp(nega), a], [1, r], [], G, [1, -r], [], 1/nega)]
80
+ return mpf2float(mp.hypercomb(h, [z], force_series=True))
81
+ except mp.libmp.NoConvergence:
82
+ def h(z):
83
+ T1 = [], [1, z-1], [z], G, [], [], 0
84
+ T2 = [-mp.exp(nega), a, z], [1, z, -1], [], G, [1], [1+z], a
85
+ return T1, T2
86
+ return mpf2float(mp.hypercomb(h, [z], maxterms=maxterms))
87
+
88
+
89
+ def main():
90
+ t0 = time()
91
+ # It would be nice to have data for larger values, but either this
92
+ # requires prohibitively large precision (dps > 800) or mpmath has
93
+ # a bug. For example, gammainc(1e20, 1e20, dps=800) returns a
94
+ # value around 0.03, while the true value should be close to 0.5
95
+ # (DLMF 8.12.15).
96
+ print(__doc__)
97
+ pwd = os.path.dirname(__file__)
98
+ r = np.logspace(4, 14, 30)
99
+ ltheta = np.logspace(np.log10(pi/4), np.log10(np.arctan(0.6)), 30)
100
+ utheta = np.logspace(np.log10(pi/4), np.log10(np.arctan(1.4)), 30)
101
+
102
+ regimes = [(gammainc, ltheta), (gammaincc, utheta)]
103
+ for func, theta in regimes:
104
+ rg, thetag = np.meshgrid(r, theta)
105
+ a, x = rg*np.cos(thetag), rg*np.sin(thetag)
106
+ a, x = a.flatten(), x.flatten()
107
+ dataset = []
108
+ for i, (a0, x0) in enumerate(zip(a, x)):
109
+ if func == gammaincc:
110
+ # Exploit the fast integer path in gammaincc whenever
111
+ # possible so that the computation doesn't take too
112
+ # long
113
+ a0, x0 = np.floor(a0), np.floor(x0)
114
+ dataset.append((a0, x0, func(a0, x0)))
115
+ dataset = np.array(dataset)
116
+ filename = os.path.join(pwd, '..', 'tests', 'data', 'local',
117
+ f'{func.__name__}.txt')
118
+ np.savetxt(filename, dataset)
119
+
120
+ print(f"{(time() - t0)/60} minutes elapsed")
121
+
122
+
123
+ if __name__ == "__main__":
124
+ main()
v12x/pylib/scipy/special/_precompute/hyp2f1_data.py ADDED
@@ -0,0 +1,484 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This script evaluates scipy's implementation of hyp2f1 against mpmath's.
2
+
3
+ Author: Albert Steppi
4
+
5
+ This script is long running and generates a large output file. With default
6
+ arguments, the generated file is roughly 700MB in size and it takes around
7
+ 40 minutes using an Intel(R) Core(TM) i5-8250U CPU with n_jobs set to 8
8
+ (full utilization). There are optional arguments which can be used to restrict
9
+ (or enlarge) the computations performed. These are described below.
10
+ The output of this script can be analyzed to identify suitable test cases and
11
+ to find parameter and argument regions where hyp2f1 needs to be improved.
12
+
13
+ The script has one mandatory positional argument for specifying the path to
14
+ the location where the output file is to be placed, and 4 optional arguments
15
+ --n_jobs, --grid_size, --regions, and --parameter_groups. --n_jobs specifies
16
+ the number of processes to use if running in parallel. The default value is 1.
17
+ The other optional arguments are explained below.
18
+
19
+ Produces a tab separated values file with 11 columns. The first four columns
20
+ contain the parameters a, b, c and the argument z. The next two contain |z| and
21
+ a region code for which region of the complex plane belongs to. The regions are
22
+
23
+ 0) z == 1
24
+ 1) |z| < 0.9 and real(z) >= 0
25
+ 2) |z| <= 1 and real(z) < 0
26
+ 3) 0.9 <= |z| <= 1 and |1 - z| < 0.9:
27
+ 4) 0.9 <= |z| <= 1 and |1 - z| >= 0.9 and real(z) >= 0:
28
+ 5) 1 < |z| < 1.1 and |1 - z| >= 0.9 and real(z) >= 0
29
+ 6) |z| > 1 and not in 5)
30
+
31
+ The --regions optional argument allows the user to specify a list of regions
32
+ to which computation will be restricted.
33
+
34
+ Parameters a, b, c are taken from a 10 * 10 * 10 grid with values at
35
+
36
+ -16, -8, -4, -2, -1, 1, 2, 4, 8, 16
37
+
38
+ with random perturbations applied.
39
+
40
+ There are 9 parameter groups handling the following cases.
41
+
42
+ 1) A, B, C, B - A, C - A, C - B, C - A - B all non-integral.
43
+ 2) B - A integral
44
+ 3) C - A integral
45
+ 4) C - B integral
46
+ 5) C - A - B integral
47
+ 6) A integral
48
+ 7) B integral
49
+ 8) C integral
50
+ 9) Wider range with c - a - b > 0.
51
+
52
+ The seventh column of the output file is an integer between 1 and 8 specifying
53
+ the parameter group as above.
54
+
55
+ The --parameter_groups optional argument allows the user to specify a list of
56
+ parameter groups to which computation will be restricted.
57
+
58
+ The argument z is taken from a grid in the box
59
+ -box_size <= real(z) <= box_size, -box_size <= imag(z) <= box_size.
60
+ with grid size specified using the optional command line argument --grid_size,
61
+ and box_size specified with the command line argument --box_size.
62
+ The default value of grid_size is 20 and the default value of box_size is 2.0,
63
+ yielding a 20 * 20 grid in the box with corners -2-2j, -2+2j, 2-2j, 2+2j.
64
+
65
+ The final four columns have the expected value of hyp2f1 for the given
66
+ parameters and argument as calculated with mpmath, the observed value
67
+ calculated with scipy's hyp2f1, the relative error, and the absolute error.
68
+
69
+ As special cases of hyp2f1 are moved from the original Fortran implementation
70
+ into Cython, this script can be used to ensure that no regressions occur and
71
+ to point out where improvements are needed.
72
+ """
73
+
74
+
75
+ import os
76
+ import csv
77
+ import argparse
78
+ import numpy as np
79
+ from itertools import product
80
+ from multiprocessing import Pool
81
+
82
+
83
+ from scipy.special import hyp2f1
84
+ from scipy.special.tests.test_hyp2f1 import mp_hyp2f1
85
+
86
+
87
+ def get_region(z):
88
+ """Assign numbers for regions where hyp2f1 must be handled differently."""
89
+ if z == 1 + 0j:
90
+ return 0
91
+ elif abs(z) < 0.9 and z.real >= 0:
92
+ return 1
93
+ elif abs(z) <= 1 and z.real < 0:
94
+ return 2
95
+ elif 0.9 <= abs(z) <= 1 and abs(1 - z) < 0.9:
96
+ return 3
97
+ elif 0.9 <= abs(z) <= 1 and abs(1 - z) >= 0.9:
98
+ return 4
99
+ elif 1 < abs(z) < 1.1 and abs(1 - z) >= 0.9 and z.real >= 0:
100
+ return 5
101
+ else:
102
+ return 6
103
+
104
+
105
+ def get_result(a, b, c, z, group):
106
+ """Get results for given parameter and value combination."""
107
+ expected, observed = mp_hyp2f1(a, b, c, z), hyp2f1(a, b, c, z)
108
+ if (
109
+ np.isnan(observed) and np.isnan(expected) or
110
+ expected == observed
111
+ ):
112
+ relative_error = 0.0
113
+ absolute_error = 0.0
114
+ elif np.isnan(observed):
115
+ # Set error to infinity if result is nan when not expected to be.
116
+ # Makes results easier to interpret.
117
+ relative_error = float("inf")
118
+ absolute_error = float("inf")
119
+ else:
120
+ absolute_error = abs(expected - observed)
121
+ relative_error = absolute_error / abs(expected)
122
+
123
+ return (
124
+ a,
125
+ b,
126
+ c,
127
+ z,
128
+ abs(z),
129
+ get_region(z),
130
+ group,
131
+ expected,
132
+ observed,
133
+ relative_error,
134
+ absolute_error,
135
+ )
136
+
137
+
138
+ def get_result_no_mp(a, b, c, z, group):
139
+ """Get results for given parameter and value combination."""
140
+ expected, observed = complex('nan'), hyp2f1(a, b, c, z)
141
+ relative_error, absolute_error = float('nan'), float('nan')
142
+ return (
143
+ a,
144
+ b,
145
+ c,
146
+ z,
147
+ abs(z),
148
+ get_region(z),
149
+ group,
150
+ expected,
151
+ observed,
152
+ relative_error,
153
+ absolute_error,
154
+ )
155
+
156
+
157
+ def get_results(params, Z, n_jobs=1, compute_mp=True):
158
+ """Batch compute results for multiple parameter and argument values.
159
+
160
+ Parameters
161
+ ----------
162
+ params : iterable
163
+ iterable of tuples of floats (a, b, c) specifying parameter values
164
+ a, b, c for hyp2f1
165
+ Z : iterable of complex
166
+ Arguments at which to evaluate hyp2f1
167
+ n_jobs : Optional[int]
168
+ Number of jobs for parallel execution.
169
+
170
+ Returns
171
+ -------
172
+ list
173
+ List of tuples of results values. See return value in source code
174
+ of `get_result`.
175
+ """
176
+ input_ = (
177
+ (a, b, c, z, group) for (a, b, c, group), z in product(params, Z)
178
+ )
179
+
180
+ with Pool(n_jobs) as pool:
181
+ rows = pool.starmap(
182
+ get_result if compute_mp else get_result_no_mp,
183
+ input_
184
+ )
185
+ return rows
186
+
187
+
188
+ def _make_hyp2f1_test_case(a, b, c, z, rtol):
189
+ """Generate string for single test case as used in test_hyp2f1.py."""
190
+ expected = mp_hyp2f1(a, b, c, z)
191
+ return (
192
+ " pytest.param(\n"
193
+ " Hyp2f1TestCase(\n"
194
+ f" a={a},\n"
195
+ f" b={b},\n"
196
+ f" c={c},\n"
197
+ f" z={z},\n"
198
+ f" expected={expected},\n"
199
+ f" rtol={rtol},\n"
200
+ " ),\n"
201
+ " ),"
202
+ )
203
+
204
+
205
+ def make_hyp2f1_test_cases(rows):
206
+ """Generate string for a list of test cases for test_hyp2f1.py.
207
+
208
+ Parameters
209
+ ----------
210
+ rows : list
211
+ List of lists of the form [a, b, c, z, rtol] where a, b, c, z are
212
+ parameters and the argument for hyp2f1 and rtol is an expected
213
+ relative error for the associated test case.
214
+
215
+ Returns
216
+ -------
217
+ str
218
+ String for a list of test cases. The output string can be printed
219
+ or saved to a file and then copied into an argument for
220
+ `pytest.mark.parameterize` within `scipy.special.tests.test_hyp2f1.py`.
221
+ """
222
+ result = "[\n"
223
+ result += '\n'.join(
224
+ _make_hyp2f1_test_case(a, b, c, z, rtol)
225
+ for a, b, c, z, rtol in rows
226
+ )
227
+ result += "\n]"
228
+ return result
229
+
230
+
231
+ def main(
232
+ outpath,
233
+ n_jobs=1,
234
+ box_size=2.0,
235
+ grid_size=20,
236
+ regions=None,
237
+ parameter_groups=None,
238
+ compute_mp=True,
239
+ ):
240
+ outpath = os.path.realpath(os.path.expanduser(outpath))
241
+
242
+ random_state = np.random.RandomState(1234)
243
+ # Parameters a, b, c selected near these values.
244
+ root_params = np.array(
245
+ [-16, -8, -4, -2, -1, 1, 2, 4, 8, 16]
246
+ )
247
+ # Perturbations to apply to root values.
248
+ perturbations = 0.1 * random_state.random_sample(
249
+ size=(3, len(root_params))
250
+ )
251
+
252
+ params = []
253
+ # Parameter group 1
254
+ # -----------------
255
+ # No integer differences. This has been confirmed for the above seed.
256
+ A = root_params + perturbations[0, :]
257
+ B = root_params + perturbations[1, :]
258
+ C = root_params + perturbations[2, :]
259
+ params.extend(
260
+ sorted(
261
+ ((a, b, c, 1) for a, b, c in product(A, B, C)),
262
+ key=lambda x: max(abs(x[0]), abs(x[1])),
263
+ )
264
+ )
265
+
266
+ # Parameter group 2
267
+ # -----------------
268
+ # B - A an integer
269
+ A = root_params + 0.5
270
+ B = root_params + 0.5
271
+ C = root_params + perturbations[1, :]
272
+ params.extend(
273
+ sorted(
274
+ ((a, b, c, 2) for a, b, c in product(A, B, C)),
275
+ key=lambda x: max(abs(x[0]), abs(x[1])),
276
+ )
277
+ )
278
+
279
+ # Parameter group 3
280
+ # -----------------
281
+ # C - A an integer
282
+ A = root_params + 0.5
283
+ B = root_params + perturbations[1, :]
284
+ C = root_params + 0.5
285
+ params.extend(
286
+ sorted(
287
+ ((a, b, c, 3) for a, b, c in product(A, B, C)),
288
+ key=lambda x: max(abs(x[0]), abs(x[1])),
289
+ )
290
+ )
291
+
292
+ # Parameter group 4
293
+ # -----------------
294
+ # C - B an integer
295
+ A = root_params + perturbations[0, :]
296
+ B = root_params + 0.5
297
+ C = root_params + 0.5
298
+ params.extend(
299
+ sorted(
300
+ ((a, b, c, 4) for a, b, c in product(A, B, C)),
301
+ key=lambda x: max(abs(x[0]), abs(x[1])),
302
+ )
303
+ )
304
+
305
+ # Parameter group 5
306
+ # -----------------
307
+ # C - A - B an integer
308
+ A = root_params + 0.25
309
+ B = root_params + 0.25
310
+ C = root_params + 0.5
311
+ params.extend(
312
+ sorted(
313
+ ((a, b, c, 5) for a, b, c in product(A, B, C)),
314
+ key=lambda x: max(abs(x[0]), abs(x[1])),
315
+ )
316
+ )
317
+
318
+ # Parameter group 6
319
+ # -----------------
320
+ # A an integer
321
+ A = root_params
322
+ B = root_params + perturbations[0, :]
323
+ C = root_params + perturbations[1, :]
324
+ params.extend(
325
+ sorted(
326
+ ((a, b, c, 6) for a, b, c in product(A, B, C)),
327
+ key=lambda x: max(abs(x[0]), abs(x[1])),
328
+ )
329
+ )
330
+
331
+ # Parameter group 7
332
+ # -----------------
333
+ # B an integer
334
+ A = root_params + perturbations[0, :]
335
+ B = root_params
336
+ C = root_params + perturbations[1, :]
337
+ params.extend(
338
+ sorted(
339
+ ((a, b, c, 7) for a, b, c in product(A, B, C)),
340
+ key=lambda x: max(abs(x[0]), abs(x[1])),
341
+ )
342
+ )
343
+
344
+ # Parameter group 8
345
+ # -----------------
346
+ # C an integer
347
+ A = root_params + perturbations[0, :]
348
+ B = root_params + perturbations[1, :]
349
+ C = root_params
350
+ params.extend(
351
+ sorted(
352
+ ((a, b, c, 8) for a, b, c in product(A, B, C)),
353
+ key=lambda x: max(abs(x[0]), abs(x[1])),
354
+ )
355
+ )
356
+
357
+ # Parameter group 9
358
+ # -----------------
359
+ # Wide range of magnitudes, c - a - b > 0.
360
+ phi = (1 + np.sqrt(5))/2
361
+ P = phi**np.arange(16)
362
+ P = np.hstack([-P, P])
363
+ group_9_params = sorted(
364
+ (
365
+ (a, b, c, 9) for a, b, c in product(P, P, P) if c - a - b > 0
366
+ ),
367
+ key=lambda x: max(abs(x[0]), abs(x[1])),
368
+ )
369
+
370
+ if parameter_groups is not None:
371
+ # Group 9 params only used if specified in arguments.
372
+ params.extend(group_9_params)
373
+ params = [
374
+ (a, b, c, group) for a, b, c, group in params
375
+ if group in parameter_groups
376
+ ]
377
+
378
+ # grid_size * grid_size grid in box with corners
379
+ # -2 - 2j, -2 + 2j, 2 - 2j, 2 + 2j
380
+ X, Y = np.meshgrid(
381
+ np.linspace(-box_size, box_size, grid_size),
382
+ np.linspace(-box_size, box_size, grid_size)
383
+ )
384
+ Z = X + Y * 1j
385
+ Z = Z.flatten().tolist()
386
+ # Add z = 1 + 0j (region 0).
387
+ Z.append(1 + 0j)
388
+ if regions is not None:
389
+ Z = [z for z in Z if get_region(z) in regions]
390
+
391
+ # Evaluate scipy and mpmath's hyp2f1 for all parameter combinations
392
+ # above against all arguments in the grid Z
393
+ rows = get_results(params, Z, n_jobs=n_jobs, compute_mp=compute_mp)
394
+
395
+ with open(outpath, "w", newline="") as f:
396
+ writer = csv.writer(f, delimiter="\t")
397
+ writer.writerow(
398
+ [
399
+ "a",
400
+ "b",
401
+ "c",
402
+ "z",
403
+ "|z|",
404
+ "region",
405
+ "parameter_group",
406
+ "expected", # mpmath's hyp2f1
407
+ "observed", # scipy's hyp2f1
408
+ "relative_error",
409
+ "absolute_error",
410
+ ]
411
+ )
412
+ for row in rows:
413
+ writer.writerow(row)
414
+
415
+
416
+ if __name__ == "__main__":
417
+ parser = argparse.ArgumentParser(
418
+ description="Test scipy's hyp2f1 against mpmath's on a grid in the"
419
+ " complex plane over a grid of parameter values. Saves output to file"
420
+ " specified in positional argument \"outpath\"."
421
+ " Caution: With default arguments, the generated output file is"
422
+ " roughly 700MB in size. Script may take several hours to finish if"
423
+ " \"--n_jobs\" is set to 1."
424
+ )
425
+ parser.add_argument(
426
+ "outpath", type=str, help="Path to output tsv file."
427
+ )
428
+ parser.add_argument(
429
+ "--n_jobs",
430
+ type=int,
431
+ default=1,
432
+ help="Number of jobs for multiprocessing.",
433
+ )
434
+ parser.add_argument(
435
+ "--box_size",
436
+ type=float,
437
+ default=2.0,
438
+ help="hyp2f1 is evaluated in box of side_length 2*box_size centered"
439
+ " at the origin."
440
+ )
441
+ parser.add_argument(
442
+ "--grid_size",
443
+ type=int,
444
+ default=20,
445
+ help="hyp2f1 is evaluated on grid_size * grid_size grid in box of side"
446
+ " length 2*box_size centered at the origin."
447
+ )
448
+ parser.add_argument(
449
+ "--parameter_groups",
450
+ type=int,
451
+ nargs='+',
452
+ default=None,
453
+ help="Restrict to supplied parameter groups. See the Docstring for"
454
+ " this module for more info on parameter groups. Calculate for all"
455
+ " parameter groups by default."
456
+ )
457
+ parser.add_argument(
458
+ "--regions",
459
+ type=int,
460
+ nargs='+',
461
+ default=None,
462
+ help="Restrict to argument z only within the supplied regions. See"
463
+ " the Docstring for this module for more info on regions. Calculate"
464
+ " for all regions by default."
465
+ )
466
+ parser.add_argument(
467
+ "--no_mp",
468
+ action='store_true',
469
+ help="If this flag is set, do not compute results with mpmath. Saves"
470
+ " time if results have already been computed elsewhere. Fills in"
471
+ " \"expected\" column with None values."
472
+ )
473
+ args = parser.parse_args()
474
+ compute_mp = not args.no_mp
475
+ print(args.parameter_groups)
476
+ main(
477
+ args.outpath,
478
+ n_jobs=args.n_jobs,
479
+ box_size=args.box_size,
480
+ grid_size=args.grid_size,
481
+ parameter_groups=args.parameter_groups,
482
+ regions=args.regions,
483
+ compute_mp=compute_mp,
484
+ )
v12x/pylib/scipy/special/_precompute/lambertw.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compute a Pade approximation for the principal branch of the
2
+ Lambert W function around 0 and compare it to various other
3
+ approximations.
4
+
5
+ """
6
+ import numpy as np
7
+
8
+ try:
9
+ import mpmath
10
+ import matplotlib.pyplot as plt
11
+ except ImportError:
12
+ pass
13
+
14
+
15
+ def lambertw_pade():
16
+ derivs = [mpmath.diff(mpmath.lambertw, 0, n=n) for n in range(6)]
17
+ p, q = mpmath.pade(derivs, 3, 2)
18
+ return p, q
19
+
20
+
21
+ def main():
22
+ print(__doc__)
23
+ with mpmath.workdps(50):
24
+ p, q = lambertw_pade()
25
+ p, q = p[::-1], q[::-1]
26
+ print(f"p = {p}")
27
+ print(f"q = {q}")
28
+
29
+ x, y = np.linspace(-1.5, 1.5, 75), np.linspace(-1.5, 1.5, 75)
30
+ x, y = np.meshgrid(x, y)
31
+ z = x + 1j*y
32
+ lambertw_std = []
33
+ for z0 in z.flatten():
34
+ lambertw_std.append(complex(mpmath.lambertw(z0)))
35
+ lambertw_std = np.array(lambertw_std).reshape(x.shape)
36
+
37
+ fig, axes = plt.subplots(nrows=3, ncols=1)
38
+ # Compare Pade approximation to true result
39
+ p = np.array([float(p0) for p0 in p])
40
+ q = np.array([float(q0) for q0 in q])
41
+ pade_approx = np.polyval(p, z)/np.polyval(q, z)
42
+ pade_err = abs(pade_approx - lambertw_std)
43
+ axes[0].pcolormesh(x, y, pade_err)
44
+ # Compare two terms of asymptotic series to true result
45
+ asy_approx = np.log(z) - np.log(np.log(z))
46
+ asy_err = abs(asy_approx - lambertw_std)
47
+ axes[1].pcolormesh(x, y, asy_err)
48
+ # Compare two terms of the series around the branch point to the
49
+ # true result
50
+ p = np.sqrt(2*(np.exp(1)*z + 1))
51
+ series_approx = -1 + p - p**2/3
52
+ series_err = abs(series_approx - lambertw_std)
53
+ im = axes[2].pcolormesh(x, y, series_err)
54
+
55
+ fig.colorbar(im, ax=axes.ravel().tolist())
56
+ plt.show()
57
+
58
+ fig, ax = plt.subplots(nrows=1, ncols=1)
59
+ pade_better = pade_err < asy_err
60
+ im = ax.pcolormesh(x, y, pade_better)
61
+ t = np.linspace(-0.3, 0.3)
62
+ ax.plot(-2.5*abs(t) - 0.2, t, 'r')
63
+ fig.colorbar(im, ax=ax)
64
+ plt.show()
65
+
66
+
67
+ if __name__ == '__main__':
68
+ main()
v12x/pylib/scipy/special/_precompute/loggamma.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Precompute series coefficients for log-Gamma."""
2
+
3
+ try:
4
+ import mpmath
5
+ except ImportError:
6
+ pass
7
+
8
+
9
+ def stirling_series(N):
10
+ with mpmath.workdps(100):
11
+ coeffs = [mpmath.bernoulli(2*n)/(2*n*(2*n - 1))
12
+ for n in range(1, N + 1)]
13
+ return coeffs
14
+
15
+
16
+ def taylor_series_at_1(N):
17
+ coeffs = []
18
+ with mpmath.workdps(100):
19
+ coeffs.append(-mpmath.euler)
20
+ for n in range(2, N + 1):
21
+ coeffs.append((-1)**n*mpmath.zeta(n)/n)
22
+ return coeffs
23
+
24
+
25
+ def main():
26
+ print(__doc__)
27
+ print()
28
+ stirling_coeffs = [mpmath.nstr(x, 20, min_fixed=0, max_fixed=0)
29
+ for x in stirling_series(8)[::-1]]
30
+ taylor_coeffs = [mpmath.nstr(x, 20, min_fixed=0, max_fixed=0)
31
+ for x in taylor_series_at_1(23)[::-1]]
32
+ print("Stirling series coefficients")
33
+ print("----------------------------")
34
+ print("\n".join(stirling_coeffs))
35
+ print()
36
+ print("Taylor series coefficients")
37
+ print("--------------------------")
38
+ print("\n".join(taylor_coeffs))
39
+ print()
40
+
41
+
42
+ if __name__ == '__main__':
43
+ main()
v12x/pylib/scipy/special/_precompute/struve_convergence.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Convergence regions of the expansions used in ``struve.c``
3
+
4
+ Note that for v >> z both functions tend rapidly to 0,
5
+ and for v << -z, they tend to infinity.
6
+
7
+ The floating-point functions over/underflow in the lower left and right
8
+ corners of the figure.
9
+
10
+
11
+ Figure legend
12
+ =============
13
+
14
+ Red region
15
+ Power series is close (1e-12) to the mpmath result
16
+
17
+ Blue region
18
+ Asymptotic series is close to the mpmath result
19
+
20
+ Green region
21
+ Bessel series is close to the mpmath result
22
+
23
+ Dotted colored lines
24
+ Boundaries of the regions
25
+
26
+ Solid colored lines
27
+ Boundaries estimated by the routine itself. These will be used
28
+ for determining which of the results to use.
29
+
30
+ Black dashed line
31
+ The line z = 0.7*|v| + 12
32
+
33
+ """
34
+ import numpy as np
35
+ import matplotlib.pyplot as plt
36
+
37
+ import mpmath
38
+
39
+
40
+ def err_metric(a, b, atol=1e-290):
41
+ m = abs(a - b) / (atol + abs(b))
42
+ m[np.isinf(b) & (a == b)] = 0
43
+ return m
44
+
45
+
46
+ def do_plot(is_h=True):
47
+ from scipy.special._ufuncs import (_struve_power_series,
48
+ _struve_asymp_large_z,
49
+ _struve_bessel_series)
50
+
51
+ vs = np.linspace(-1000, 1000, 91)
52
+ zs = np.sort(np.r_[1e-5, 1.0, np.linspace(0, 700, 91)[1:]])
53
+
54
+ rp = _struve_power_series(vs[:,None], zs[None,:], is_h)
55
+ ra = _struve_asymp_large_z(vs[:,None], zs[None,:], is_h)
56
+ rb = _struve_bessel_series(vs[:,None], zs[None,:], is_h)
57
+
58
+ mpmath.mp.dps = 50
59
+ if is_h:
60
+ def sh(v, z):
61
+ return float(mpmath.struveh(mpmath.mpf(v), mpmath.mpf(z)))
62
+ else:
63
+ def sh(v, z):
64
+ return float(mpmath.struvel(mpmath.mpf(v), mpmath.mpf(z)))
65
+ ex = np.vectorize(sh, otypes='d')(vs[:,None], zs[None,:])
66
+
67
+ err_a = err_metric(ra[0], ex) + 1e-300
68
+ err_p = err_metric(rp[0], ex) + 1e-300
69
+ err_b = err_metric(rb[0], ex) + 1e-300
70
+
71
+ err_est_a = abs(ra[1]/ra[0])
72
+ err_est_p = abs(rp[1]/rp[0])
73
+ err_est_b = abs(rb[1]/rb[0])
74
+
75
+ z_cutoff = 0.7*abs(vs) + 12
76
+
77
+ levels = [-1000, -12]
78
+
79
+ plt.cla()
80
+
81
+ plt.hold(1)
82
+ plt.contourf(vs, zs, np.log10(err_p).T,
83
+ levels=levels, colors=['r', 'r'], alpha=0.1)
84
+ plt.contourf(vs, zs, np.log10(err_a).T,
85
+ levels=levels, colors=['b', 'b'], alpha=0.1)
86
+ plt.contourf(vs, zs, np.log10(err_b).T,
87
+ levels=levels, colors=['g', 'g'], alpha=0.1)
88
+
89
+ plt.contour(vs, zs, np.log10(err_p).T,
90
+ levels=levels, colors=['r', 'r'], linestyles=[':', ':'])
91
+ plt.contour(vs, zs, np.log10(err_a).T,
92
+ levels=levels, colors=['b', 'b'], linestyles=[':', ':'])
93
+ plt.contour(vs, zs, np.log10(err_b).T,
94
+ levels=levels, colors=['g', 'g'], linestyles=[':', ':'])
95
+
96
+ lp = plt.contour(vs, zs, np.log10(err_est_p).T,
97
+ levels=levels, colors=['r', 'r'], linestyles=['-', '-'])
98
+ la = plt.contour(vs, zs, np.log10(err_est_a).T,
99
+ levels=levels, colors=['b', 'b'], linestyles=['-', '-'])
100
+ lb = plt.contour(vs, zs, np.log10(err_est_b).T,
101
+ levels=levels, colors=['g', 'g'], linestyles=['-', '-'])
102
+
103
+ plt.clabel(lp, fmt={-1000: 'P', -12: 'P'})
104
+ plt.clabel(la, fmt={-1000: 'A', -12: 'A'})
105
+ plt.clabel(lb, fmt={-1000: 'B', -12: 'B'})
106
+
107
+ plt.plot(vs, z_cutoff, 'k--')
108
+
109
+ plt.xlim(vs.min(), vs.max())
110
+ plt.ylim(zs.min(), zs.max())
111
+
112
+ plt.xlabel('v')
113
+ plt.ylabel('z')
114
+
115
+
116
+ def main():
117
+ plt.clf()
118
+ plt.subplot(121)
119
+ do_plot(True)
120
+ plt.title('Struve H')
121
+
122
+ plt.subplot(122)
123
+ do_plot(False)
124
+ plt.title('Struve L')
125
+
126
+ plt.savefig('struve_convergence.png')
127
+ plt.show()
128
+
129
+
130
+ if __name__ == "__main__":
131
+ main()
v12x/pylib/scipy/special/_precompute/utils.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ try:
2
+ import mpmath as mp
3
+ except ImportError:
4
+ pass
5
+
6
+ try:
7
+ from sympy.abc import x
8
+ except ImportError:
9
+ pass
10
+
11
+
12
+ def lagrange_inversion(a):
13
+ """Given a series
14
+
15
+ f(x) = a[1]*x + a[2]*x**2 + ... + a[n-1]*x**(n - 1),
16
+
17
+ use the Lagrange inversion formula to compute a series
18
+
19
+ g(x) = b[1]*x + b[2]*x**2 + ... + b[n-1]*x**(n - 1)
20
+
21
+ so that f(g(x)) = g(f(x)) = x mod x**n. We must have a[0] = 0, so
22
+ necessarily b[0] = 0 too.
23
+
24
+ The algorithm is naive and could be improved, but speed isn't an
25
+ issue here and it's easy to read.
26
+
27
+ """
28
+ n = len(a)
29
+ f = sum(a[i]*x**i for i in range(n))
30
+ h = (x/f).series(x, 0, n).removeO()
31
+ hpower = [h**0]
32
+ for k in range(n):
33
+ hpower.append((hpower[-1]*h).expand())
34
+ b = [mp.mpf(0)]
35
+ for k in range(1, n):
36
+ b.append(hpower[k].coeff(x, k - 1)/k)
37
+ b = [mp.mpf(x) for x in b]
38
+ return b
v12x/pylib/scipy/special/_precompute/wright_bessel.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Precompute coefficients of several series expansions
2
+ of Wright's generalized Bessel function Phi(a, b, x).
3
+
4
+ See https://dlmf.nist.gov/10.46.E1 with rho=a, beta=b, z=x.
5
+ """
6
+ from argparse import ArgumentParser, RawTextHelpFormatter
7
+ import numpy as np
8
+ from scipy.integrate import quad
9
+ from scipy.optimize import minimize_scalar, curve_fit
10
+ from time import time
11
+
12
+ try:
13
+ import sympy
14
+ from sympy import EulerGamma, Rational, S, Sum, \
15
+ factorial, gamma, gammasimp, pi, polygamma, symbols, zeta
16
+ from sympy.polys.polyfuncs import horner
17
+ except ImportError:
18
+ pass
19
+
20
+
21
+ def series_small_a():
22
+ """Tylor series expansion of Phi(a, b, x) in a=0 up to order 5.
23
+ """
24
+ order = 5
25
+ a, b, x, k = symbols("a b x k")
26
+ A = [] # terms with a
27
+ X = [] # terms with x
28
+ B = [] # terms with b (polygammas)
29
+ # Phi(a, b, x) = exp(x)/gamma(b) * sum(A[i] * X[i] * B[i])
30
+ expression = Sum(x**k/factorial(k)/gamma(a*k+b), (k, 0, S.Infinity))
31
+ expression = gamma(b)/sympy.exp(x) * expression
32
+
33
+ # nth term of taylor series in a=0: a^n/n! * (d^n Phi(a, b, x)/da^n at a=0)
34
+ for n in range(0, order+1):
35
+ term = expression.diff(a, n).subs(a, 0).simplify().doit()
36
+ # set the whole bracket involving polygammas to 1
37
+ x_part = (term.subs(polygamma(0, b), 1)
38
+ .replace(polygamma, lambda *args: 0))
39
+ # sign convention: x part always positive
40
+ x_part *= (-1)**n
41
+
42
+ A.append(a**n/factorial(n))
43
+ X.append(horner(x_part))
44
+ B.append(horner((term/x_part).simplify()))
45
+
46
+ s = "Tylor series expansion of Phi(a, b, x) in a=0 up to order 5.\n"
47
+ s += "Phi(a, b, x) = exp(x)/gamma(b) * sum(A[i] * X[i] * B[i], i=0..5)\n"
48
+ for name, c in zip(['A', 'X', 'B'], [A, X, B]):
49
+ for i in range(len(c)):
50
+ s += f"\n{name}[{i}] = " + str(c[i])
51
+ return s
52
+
53
+
54
+ # expansion of digamma
55
+ def dg_series(z, n):
56
+ """Symbolic expansion of digamma(z) in z=0 to order n.
57
+
58
+ See https://dlmf.nist.gov/5.7.E4 and with https://dlmf.nist.gov/5.5.E2
59
+ """
60
+ k = symbols("k")
61
+ return -1/z - EulerGamma + \
62
+ sympy.summation((-1)**k * zeta(k) * z**(k-1), (k, 2, n+1))
63
+
64
+
65
+ def pg_series(k, z, n):
66
+ """Symbolic expansion of polygamma(k, z) in z=0 to order n."""
67
+ return sympy.diff(dg_series(z, n+k), z, k)
68
+
69
+
70
+ def series_small_a_small_b():
71
+ """Tylor series expansion of Phi(a, b, x) in a=0 and b=0 up to order 5.
72
+
73
+ Be aware of cancellation of poles in b=0 of digamma(b)/Gamma(b) and
74
+ polygamma functions.
75
+
76
+ digamma(b)/Gamma(b) = -1 - 2*M_EG*b + O(b^2)
77
+ digamma(b)^2/Gamma(b) = 1/b + 3*M_EG + b*(-5/12*PI^2+7/2*M_EG^2) + O(b^2)
78
+ polygamma(1, b)/Gamma(b) = 1/b + M_EG + b*(1/12*PI^2 + 1/2*M_EG^2) + O(b^2)
79
+ and so on.
80
+ """
81
+ order = 5
82
+ a, b, x, k = symbols("a b x k")
83
+ M_PI, M_EG, M_Z3 = symbols("M_PI M_EG M_Z3")
84
+ c_subs = {pi: M_PI, EulerGamma: M_EG, zeta(3): M_Z3}
85
+ A = [] # terms with a
86
+ X = [] # terms with x
87
+ B = [] # terms with b (polygammas expanded)
88
+ C = [] # terms that generate B
89
+ # Phi(a, b, x) = exp(x) * sum(A[i] * X[i] * B[i])
90
+ # B[0] = 1
91
+ # B[k] = sum(C[k] * b**k/k!, k=0..)
92
+ # Note: C[k] can be obtained from a series expansion of 1/gamma(b).
93
+ expression = gamma(b)/sympy.exp(x) * \
94
+ Sum(x**k/factorial(k)/gamma(a*k+b), (k, 0, S.Infinity))
95
+
96
+ # nth term of taylor series in a=0: a^n/n! * (d^n Phi(a, b, x)/da^n at a=0)
97
+ for n in range(0, order+1):
98
+ term = expression.diff(a, n).subs(a, 0).simplify().doit()
99
+ # set the whole bracket involving polygammas to 1
100
+ x_part = (term.subs(polygamma(0, b), 1)
101
+ .replace(polygamma, lambda *args: 0))
102
+ # sign convention: x part always positive
103
+ x_part *= (-1)**n
104
+ # expansion of polygamma part with 1/gamma(b)
105
+ pg_part = term/x_part/gamma(b)
106
+ if n >= 1:
107
+ # Note: highest term is digamma^n
108
+ pg_part = pg_part.replace(polygamma,
109
+ lambda k, x: pg_series(k, x, order+1+n))
110
+ pg_part = (pg_part.series(b, 0, n=order+1-n)
111
+ .removeO()
112
+ .subs(polygamma(2, 1), -2*zeta(3))
113
+ .simplify()
114
+ )
115
+
116
+ A.append(a**n/factorial(n))
117
+ X.append(horner(x_part))
118
+ B.append(pg_part)
119
+
120
+ # Calculate C and put in the k!
121
+ C = sympy.Poly(B[1].subs(c_subs), b).coeffs()
122
+ C.reverse()
123
+ for i in range(len(C)):
124
+ C[i] = (C[i] * factorial(i)).simplify()
125
+
126
+ s = "Tylor series expansion of Phi(a, b, x) in a=0 and b=0 up to order 5."
127
+ s += "\nPhi(a, b, x) = exp(x) * sum(A[i] * X[i] * B[i], i=0..5)\n"
128
+ s += "B[0] = 1\n"
129
+ s += "B[i] = sum(C[k+i-1] * b**k/k!, k=0..)\n"
130
+ s += "\nM_PI = pi"
131
+ s += "\nM_EG = EulerGamma"
132
+ s += "\nM_Z3 = zeta(3)"
133
+ for name, c in zip(['A', 'X'], [A, X]):
134
+ for i in range(len(c)):
135
+ s += f"\n{name}[{i}] = "
136
+ s += str(c[i])
137
+ # For C, do also compute the values numerically
138
+ for i in range(len(C)):
139
+ s += f"\n# C[{i}] = "
140
+ s += str(C[i])
141
+ s += f"\nC[{i}] = "
142
+ s += str(C[i].subs({M_EG: EulerGamma, M_PI: pi, M_Z3: zeta(3)})
143
+ .evalf(17))
144
+
145
+ # Does B have the assumed structure?
146
+ s += "\n\nTest if B[i] does have the assumed structure."
147
+ s += "\nC[i] are derived from B[1] alone."
148
+ s += "\nTest B[2] == C[1] + b*C[2] + b^2/2*C[3] + b^3/6*C[4] + .."
149
+ test = sum([b**k/factorial(k) * C[k+1] for k in range(order-1)])
150
+ test = (test - B[2].subs(c_subs)).simplify()
151
+ s += f"\ntest successful = {test==S(0)}"
152
+ s += "\nTest B[3] == C[2] + b*C[3] + b^2/2*C[4] + .."
153
+ test = sum([b**k/factorial(k) * C[k+2] for k in range(order-2)])
154
+ test = (test - B[3].subs(c_subs)).simplify()
155
+ s += f"\ntest successful = {test==S(0)}"
156
+ return s
157
+
158
+
159
+ def asymptotic_series():
160
+ """Asymptotic expansion for large x.
161
+
162
+ Phi(a, b, x) ~ Z^(1/2-b) * exp((1+a)/a * Z) * sum_k (-1)^k * C_k / Z^k
163
+ Z = (a*x)^(1/(1+a))
164
+
165
+ Wright (1935) lists the coefficients C_0 and C_1 (he calls them a_0 and
166
+ a_1). With slightly different notation, Paris (2017) lists coefficients
167
+ c_k up to order k=3.
168
+ Paris (2017) uses ZP = (1+a)/a * Z (ZP = Z of Paris) and
169
+ C_k = C_0 * (-a/(1+a))^k * c_k
170
+ """
171
+ order = 8
172
+
173
+ class g(sympy.Function):
174
+ """Helper function g according to Wright (1935)
175
+
176
+ g(n, rho, v) = (1 + (rho+2)/3 * v + (rho+2)*(rho+3)/(2*3) * v^2 + ...)
177
+
178
+ Note: Wright (1935) uses square root of above definition.
179
+ """
180
+ nargs = 3
181
+
182
+ @classmethod
183
+ def eval(cls, n, rho, v):
184
+ if not n >= 0:
185
+ raise ValueError("must have n >= 0")
186
+ elif n == 0:
187
+ return 1
188
+ else:
189
+ return g(n-1, rho, v) \
190
+ + gammasimp(gamma(rho+2+n)/gamma(rho+2)) \
191
+ / gammasimp(gamma(3+n)/gamma(3))*v**n
192
+
193
+ class coef_C(sympy.Function):
194
+ """Calculate coefficients C_m for integer m.
195
+
196
+ C_m is the coefficient of v^(2*m) in the Taylor expansion in v=0 of
197
+ Gamma(m+1/2)/(2*pi) * (2/(rho+1))^(m+1/2) * (1-v)^(-b)
198
+ * g(rho, v)^(-m-1/2)
199
+ """
200
+ nargs = 3
201
+
202
+ @classmethod
203
+ def eval(cls, m, rho, beta):
204
+ if not m >= 0:
205
+ raise ValueError("must have m >= 0")
206
+
207
+ v = symbols("v")
208
+ expression = (1-v)**(-beta) * g(2*m, rho, v)**(-m-Rational(1, 2))
209
+ res = expression.diff(v, 2*m).subs(v, 0) / factorial(2*m)
210
+ res = res * (gamma(m + Rational(1, 2)) / (2*pi)
211
+ * (2/(rho+1))**(m + Rational(1, 2)))
212
+ return res
213
+
214
+ # in order to have nice ordering/sorting of expressions, we set a = xa.
215
+ xa, b, xap1 = symbols("xa b xap1")
216
+ C0 = coef_C(0, xa, b)
217
+ # a1 = a(1, rho, beta)
218
+ s = "Asymptotic expansion for large x\n"
219
+ s += "Phi(a, b, x) = Z**(1/2-b) * exp((1+a)/a * Z) \n"
220
+ s += " * sum((-1)**k * C[k]/Z**k, k=0..6)\n\n"
221
+ s += "Z = pow(a * x, 1/(1+a))\n"
222
+ s += "A[k] = pow(a, k)\n"
223
+ s += "B[k] = pow(b, k)\n"
224
+ s += "Ap1[k] = pow(1+a, k)\n\n"
225
+ s += "C[0] = 1./sqrt(2. * M_PI * Ap1[1])\n"
226
+ for i in range(1, order+1):
227
+ expr = (coef_C(i, xa, b) / (C0/(1+xa)**i)).simplify()
228
+ factor = [x.denominator() for x in sympy.Poly(expr).coeffs()]
229
+ factor = sympy.lcm(factor)
230
+ expr = (expr * factor).simplify().collect(b, sympy.factor)
231
+ expr = expr.xreplace({xa+1: xap1})
232
+ s += f"C[{i}] = C[0] / ({factor} * Ap1[{i}])\n"
233
+ s += f"C[{i}] *= {str(expr)}\n\n"
234
+ import re
235
+ re_a = re.compile(r'xa\*\*(\d+)')
236
+ s = re_a.sub(r'A[\1]', s)
237
+ re_b = re.compile(r'b\*\*(\d+)')
238
+ s = re_b.sub(r'B[\1]', s)
239
+ s = s.replace('xap1', 'Ap1[1]')
240
+ s = s.replace('xa', 'a')
241
+ # max integer = 2^31-1 = 2,147,483,647. Solution: Put a point after 10
242
+ # or more digits.
243
+ re_digits = re.compile(r'(\d{10,})')
244
+ s = re_digits.sub(r'\1.', s)
245
+ return s
246
+
247
+
248
+ def optimal_epsilon_integral():
249
+ """Fit optimal choice of epsilon for integral representation.
250
+
251
+ The integrand of
252
+ int_0^pi P(eps, a, b, x, phi) * dphi
253
+ can exhibit oscillatory behaviour. It stems from the cosine of P and can be
254
+ minimized by minimizing the arc length of the argument
255
+ f(phi) = eps * sin(phi) - x * eps^(-a) * sin(a * phi) + (1 - b) * phi
256
+ of cos(f(phi)).
257
+ We minimize the arc length in eps for a grid of values (a, b, x) and fit a
258
+ parametric function to it.
259
+ """
260
+ def fp(eps, a, b, x, phi):
261
+ """Derivative of f w.r.t. phi."""
262
+ eps_a = np.power(1. * eps, -a)
263
+ return eps * np.cos(phi) - a * x * eps_a * np.cos(a * phi) + 1 - b
264
+
265
+ def arclength(eps, a, b, x, epsrel=1e-2, limit=100):
266
+ """Compute Arc length of f.
267
+
268
+ Note that the arc length of a function f from t0 to t1 is given by
269
+ int_t0^t1 sqrt(1 + f'(t)^2) dt
270
+ """
271
+ return quad(lambda phi: np.sqrt(1 + fp(eps, a, b, x, phi)**2),
272
+ 0, np.pi,
273
+ epsrel=epsrel, limit=100)[0]
274
+
275
+ # grid of minimal arc length values
276
+ data_a = [1e-3, 0.1, 0.5, 0.9, 1, 2, 4, 5, 6, 8]
277
+ data_b = [0, 1, 4, 7, 10]
278
+ data_x = [1, 1.5, 2, 4, 10, 20, 50, 100, 200, 500, 1e3, 5e3, 1e4]
279
+ data_a, data_b, data_x = np.meshgrid(data_a, data_b, data_x)
280
+ data_a, data_b, data_x = (data_a.flatten(), data_b.flatten(),
281
+ data_x.flatten())
282
+ best_eps = []
283
+ for i in range(data_x.size):
284
+ best_eps.append(
285
+ minimize_scalar(lambda eps: arclength(eps, data_a[i], data_b[i],
286
+ data_x[i]),
287
+ bounds=(1e-3, 1000),
288
+ method='Bounded', options={'xatol': 1e-3}).x
289
+ )
290
+ best_eps = np.array(best_eps)
291
+ # pandas would be nice, but here a dictionary is enough
292
+ df = {'a': data_a,
293
+ 'b': data_b,
294
+ 'x': data_x,
295
+ 'eps': best_eps,
296
+ }
297
+
298
+ def func(data, A0, A1, A2, A3, A4, A5):
299
+ """Compute parametric function to fit."""
300
+ a = data['a']
301
+ b = data['b']
302
+ x = data['x']
303
+ return (A0 * b * np.exp(-0.5 * a)
304
+ + np.exp(A1 + 1 / (1 + a) * np.log(x) - A2 * np.exp(-A3 * a)
305
+ + A4 / (1 + np.exp(A5 * a))))
306
+
307
+ func_params = list(curve_fit(func, df, df['eps'], method='trf')[0])
308
+
309
+ s = "Fit optimal eps for integrand P via minimal arc length\n"
310
+ s += "with parametric function:\n"
311
+ s += "optimal_eps = (A0 * b * exp(-a/2) + exp(A1 + 1 / (1 + a) * log(x)\n"
312
+ s += " - A2 * exp(-A3 * a) + A4 / (1 + exp(A5 * a)))\n\n"
313
+ s += "Fitted parameters A0 to A5 are:\n"
314
+ s += ', '.join([f'{x:.5g}' for x in func_params])
315
+ return s
316
+
317
+
318
+ def main():
319
+ t0 = time()
320
+ parser = ArgumentParser(description=__doc__,
321
+ formatter_class=RawTextHelpFormatter)
322
+ parser.add_argument('action', type=int, choices=[1, 2, 3, 4],
323
+ help='chose what expansion to precompute\n'
324
+ '1 : Series for small a\n'
325
+ '2 : Series for small a and small b\n'
326
+ '3 : Asymptotic series for large x\n'
327
+ ' This may take some time (>4h).\n'
328
+ '4 : Fit optimal eps for integral representation.'
329
+ )
330
+ args = parser.parse_args()
331
+
332
+ switch = {1: lambda: print(series_small_a()),
333
+ 2: lambda: print(series_small_a_small_b()),
334
+ 3: lambda: print(asymptotic_series()),
335
+ 4: lambda: print(optimal_epsilon_integral())
336
+ }
337
+ switch.get(args.action, lambda: print("Invalid input."))()
338
+ print(f"\n{(time() - t0)/60:.1f} minutes elapsed.\n")
339
+
340
+
341
+ if __name__ == '__main__':
342
+ main()
v12x/pylib/scipy/special/_precompute/wright_bessel_data.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compute a grid of values for Wright's generalized Bessel function
2
+ and save the values to data files for use in tests. Using mpmath directly in
3
+ tests would take too long.
4
+
5
+ This takes about 10 minutes to run on a 2.7 GHz i7 Macbook Pro.
6
+ """
7
+ from functools import lru_cache
8
+ import os
9
+ from time import time
10
+
11
+ import numpy as np
12
+ from scipy.special._mptestutils import mpf2float
13
+
14
+ try:
15
+ import mpmath as mp
16
+ except ImportError:
17
+ pass
18
+
19
+ # exp_inf: smallest value x for which exp(x) == inf
20
+ exp_inf = 709.78271289338403
21
+
22
+
23
+ # 64 Byte per value
24
+ @lru_cache(maxsize=100_000)
25
+ def rgamma_cached(x, dps):
26
+ with mp.workdps(dps):
27
+ return mp.rgamma(x)
28
+
29
+
30
+ def mp_wright_bessel(a, b, x, dps=50, maxterms=2000):
31
+ """Compute Wright's generalized Bessel function as Series with mpmath.
32
+ """
33
+ with mp.workdps(dps):
34
+ a, b, x = mp.mpf(a), mp.mpf(b), mp.mpf(x)
35
+ res = mp.nsum(lambda k: x**k / mp.fac(k)
36
+ * rgamma_cached(a * k + b, dps=dps),
37
+ [0, mp.inf],
38
+ tol=dps, method='s', steps=[maxterms]
39
+ )
40
+ return mpf2float(res)
41
+
42
+
43
+ def main():
44
+ t0 = time()
45
+ print(__doc__)
46
+ pwd = os.path.dirname(__file__)
47
+ eps = np.finfo(float).eps * 100
48
+
49
+ a_range = np.array([eps,
50
+ 1e-4 * (1 - eps), 1e-4, 1e-4 * (1 + eps),
51
+ 1e-3 * (1 - eps), 1e-3, 1e-3 * (1 + eps),
52
+ 0.1, 0.5,
53
+ 1 * (1 - eps), 1, 1 * (1 + eps),
54
+ 1.5, 2, 4.999, 5, 10])
55
+ b_range = np.array([0, eps, 1e-10, 1e-5, 0.1, 1, 2, 10, 20, 100])
56
+ x_range = np.array([0, eps, 1 - eps, 1, 1 + eps,
57
+ 1.5,
58
+ 2 - eps, 2, 2 + eps,
59
+ 9 - eps, 9, 9 + eps,
60
+ 10 * (1 - eps), 10, 10 * (1 + eps),
61
+ 100 * (1 - eps), 100, 100 * (1 + eps),
62
+ 500, exp_inf, 1e3, 1e5, 1e10, 1e20])
63
+
64
+ a_range, b_range, x_range = np.meshgrid(a_range, b_range, x_range,
65
+ indexing='ij')
66
+ a_range = a_range.flatten()
67
+ b_range = b_range.flatten()
68
+ x_range = x_range.flatten()
69
+
70
+ # filter out some values, especially too large x
71
+ bool_filter = ~((a_range < 5e-3) & (x_range >= exp_inf))
72
+ bool_filter = bool_filter & ~((a_range < 0.2) & (x_range > exp_inf))
73
+ bool_filter = bool_filter & ~((a_range < 0.5) & (x_range > 1e3))
74
+ bool_filter = bool_filter & ~((a_range < 0.56) & (x_range > 5e3))
75
+ bool_filter = bool_filter & ~((a_range < 1) & (x_range > 1e4))
76
+ bool_filter = bool_filter & ~((a_range < 1.4) & (x_range > 1e5))
77
+ bool_filter = bool_filter & ~((a_range < 1.8) & (x_range > 1e6))
78
+ bool_filter = bool_filter & ~((a_range < 2.2) & (x_range > 1e7))
79
+ bool_filter = bool_filter & ~((a_range < 2.5) & (x_range > 1e8))
80
+ bool_filter = bool_filter & ~((a_range < 2.9) & (x_range > 1e9))
81
+ bool_filter = bool_filter & ~((a_range < 3.3) & (x_range > 1e10))
82
+ bool_filter = bool_filter & ~((a_range < 3.7) & (x_range > 1e11))
83
+ bool_filter = bool_filter & ~((a_range < 4) & (x_range > 1e12))
84
+ bool_filter = bool_filter & ~((a_range < 4.4) & (x_range > 1e13))
85
+ bool_filter = bool_filter & ~((a_range < 4.7) & (x_range > 1e14))
86
+ bool_filter = bool_filter & ~((a_range < 5.1) & (x_range > 1e15))
87
+ bool_filter = bool_filter & ~((a_range < 5.4) & (x_range > 1e16))
88
+ bool_filter = bool_filter & ~((a_range < 5.8) & (x_range > 1e17))
89
+ bool_filter = bool_filter & ~((a_range < 6.2) & (x_range > 1e18))
90
+ bool_filter = bool_filter & ~((a_range < 6.2) & (x_range > 1e18))
91
+ bool_filter = bool_filter & ~((a_range < 6.5) & (x_range > 1e19))
92
+ bool_filter = bool_filter & ~((a_range < 6.9) & (x_range > 1e20))
93
+
94
+ # filter out known values that do not meet the required numerical accuracy
95
+ # see test test_wright_data_grid_failures
96
+ failing = np.array([
97
+ [0.1, 100, 709.7827128933841],
98
+ [0.5, 10, 709.7827128933841],
99
+ [0.5, 10, 1000],
100
+ [0.5, 100, 1000],
101
+ [1, 20, 100000],
102
+ [1, 100, 100000],
103
+ [1.0000000000000222, 20, 100000],
104
+ [1.0000000000000222, 100, 100000],
105
+ [1.5, 0, 500],
106
+ [1.5, 2.220446049250313e-14, 500],
107
+ [1.5, 1.e-10, 500],
108
+ [1.5, 1.e-05, 500],
109
+ [1.5, 0.1, 500],
110
+ [1.5, 20, 100000],
111
+ [1.5, 100, 100000],
112
+ ]).tolist()
113
+
114
+ does_fail = np.full_like(a_range, False, dtype=bool)
115
+ for i in range(x_range.size):
116
+ if [a_range[i], b_range[i], x_range[i]] in failing:
117
+ does_fail[i] = True
118
+
119
+ # filter and flatten
120
+ a_range = a_range[bool_filter]
121
+ b_range = b_range[bool_filter]
122
+ x_range = x_range[bool_filter]
123
+ does_fail = does_fail[bool_filter]
124
+
125
+ dataset = []
126
+ print(f"Computing {x_range.size} single points.")
127
+ print("Tests will fail for the following data points:")
128
+ for i in range(x_range.size):
129
+ a = a_range[i]
130
+ b = b_range[i]
131
+ x = x_range[i]
132
+ # take care of difficult corner cases
133
+ maxterms = 1000
134
+ if a < 1e-6 and x >= exp_inf/10:
135
+ maxterms = 2000
136
+ f = mp_wright_bessel(a, b, x, maxterms=maxterms)
137
+ if does_fail[i]:
138
+ print("failing data point a, b, x, value = "
139
+ f"[{a}, {b}, {x}, {f}]")
140
+ else:
141
+ dataset.append((a, b, x, f))
142
+ dataset = np.array(dataset)
143
+
144
+ filename = os.path.join(pwd, '..', 'tests', 'data', 'local',
145
+ 'wright_bessel.txt')
146
+ np.savetxt(filename, dataset)
147
+
148
+ print(f"{(time() - t0)/60:.1f} minutes elapsed")
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()
v12x/pylib/scipy/special/_precompute/wrightomega.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ try:
4
+ import mpmath
5
+ except ImportError:
6
+ pass
7
+
8
+
9
+ def mpmath_wrightomega(x):
10
+ return mpmath.lambertw(mpmath.exp(x), mpmath.mpf('-0.5'))
11
+
12
+
13
+ def wrightomega_series_error(x):
14
+ series = x
15
+ desired = mpmath_wrightomega(x)
16
+ return abs(series - desired) / desired
17
+
18
+
19
+ def wrightomega_exp_error(x):
20
+ exponential_approx = mpmath.exp(x)
21
+ desired = mpmath_wrightomega(x)
22
+ return abs(exponential_approx - desired) / desired
23
+
24
+
25
+ def main():
26
+ desired_error = 2 * np.finfo(float).eps
27
+ print('Series Error')
28
+ for x in [1e5, 1e10, 1e15, 1e20]:
29
+ with mpmath.workdps(100):
30
+ error = wrightomega_series_error(x)
31
+ print(x, error, error < desired_error)
32
+
33
+ print('Exp error')
34
+ for x in [-10, -25, -50, -100, -200, -400, -700, -740]:
35
+ with mpmath.workdps(100):
36
+ error = wrightomega_exp_error(x)
37
+ print(x, error, error < desired_error)
38
+
39
+
40
+ if __name__ == '__main__':
41
+ main()
v12x/pylib/scipy/special/_precompute/zetac.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compute the Taylor series for zeta(x) - 1 around x = 0."""
2
+ try:
3
+ import mpmath
4
+ except ImportError:
5
+ pass
6
+
7
+
8
+ def zetac_series(N):
9
+ coeffs = []
10
+ with mpmath.workdps(100):
11
+ coeffs.append(-1.5)
12
+ for n in range(1, N):
13
+ coeff = mpmath.diff(mpmath.zeta, 0, n)/mpmath.factorial(n)
14
+ coeffs.append(coeff)
15
+ return coeffs
16
+
17
+
18
+ def main():
19
+ print(__doc__)
20
+ coeffs = zetac_series(10)
21
+ coeffs = [mpmath.nstr(x, 20, min_fixed=0, max_fixed=0)
22
+ for x in coeffs]
23
+ print("\n".join(coeffs[::-1]))
24
+
25
+
26
+ if __name__ == '__main__':
27
+ main()
v12x/pylib/scipy/special/_sf_error.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Warnings and Exceptions that can be raised by special functions."""
2
+ import warnings
3
+
4
+
5
+ class SpecialFunctionWarning(Warning):
6
+ """Warning that can be emitted by special functions."""
7
+ pass
8
+
9
+
10
+ warnings.simplefilter("always", category=SpecialFunctionWarning)
11
+
12
+
13
+ class SpecialFunctionError(Exception):
14
+ """Exception that can be raised by special functions."""
15
+ pass
v12x/pylib/scipy/special/_spfun_stats.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Last Change: Sat Mar 21 02:00 PM 2009 J
2
+
3
+ # Copyright (c) 2001, 2002 Enthought, Inc.
4
+ #
5
+ # All rights reserved.
6
+ #
7
+ # Redistribution and use in source and binary forms, with or without
8
+ # modification, are permitted provided that the following conditions are met:
9
+ #
10
+ # a. Redistributions of source code must retain the above copyright notice,
11
+ # this list of conditions and the following disclaimer.
12
+ # b. Redistributions in binary form must reproduce the above copyright
13
+ # notice, this list of conditions and the following disclaimer in the
14
+ # documentation and/or other materials provided with the distribution.
15
+ # c. Neither the name of the Enthought nor the names of its contributors
16
+ # may be used to endorse or promote products derived from this software
17
+ # without specific prior written permission.
18
+ #
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
24
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28
+ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
29
+ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
30
+ # DAMAGE.
31
+
32
+ """Some more special functions which may be useful for multivariate statistical
33
+ analysis."""
34
+
35
+ import numpy as np
36
+
37
+ import scipy.special._gufuncs as _gufuncs
38
+ from scipy.special import gammaln as loggam
39
+ from scipy.special._ufunc_tools import _with_cache_optimization
40
+
41
+
42
+ __all__ = ['multigammaln']
43
+
44
+
45
+ def multigammaln(a, d):
46
+ r"""Returns the log of multivariate gamma, also sometimes called the
47
+ generalized gamma.
48
+
49
+ Parameters
50
+ ----------
51
+ a : ndarray
52
+ The multivariate gamma is computed for each item of `a`.
53
+ d : int
54
+ The dimension of the space of integration.
55
+
56
+ Returns
57
+ -------
58
+ res : ndarray
59
+ The values of the log multivariate gamma at the given points `a`.
60
+
61
+ Notes
62
+ -----
63
+ The formal definition of the multivariate gamma of dimension d for a real
64
+ `a` is
65
+
66
+ .. math::
67
+
68
+ \Gamma_d(a) = \int_{A>0} e^{-tr(A)} |A|^{a - (d+1)/2} dA
69
+
70
+ with the condition :math:`a > (d-1)/2`, and :math:`A > 0` being the set of
71
+ all the positive definite matrices of dimension `d`. Note that `a` is a
72
+ scalar: the integrand only is multivariate, the argument is not (the
73
+ function is defined over a subset of the real set).
74
+
75
+ This can be proven to be equal to the much friendlier equation
76
+
77
+ .. math::
78
+
79
+ \Gamma_d(a) = \pi^{d(d-1)/4} \prod_{i=1}^{d} \Gamma(a - (i-1)/2).
80
+
81
+ References
82
+ ----------
83
+ R. J. Muirhead, Aspects of multivariate statistical theory (Wiley Series in
84
+ probability and mathematical statistics).
85
+
86
+ Examples
87
+ --------
88
+ >>> import numpy as np
89
+ >>> from scipy.special import multigammaln, gammaln
90
+ >>> a = 23.5
91
+ >>> d = 10
92
+ >>> multigammaln(a, d)
93
+ 454.1488605074416
94
+
95
+ Verify that the result agrees with the logarithm of the equation
96
+ shown above:
97
+
98
+ >>> d*(d-1)/4*np.log(np.pi) + gammaln(a - 0.5*np.arange(0, d)).sum()
99
+ 454.1488605074416
100
+ """
101
+ a = np.asarray(a)
102
+ # Support for 0d arrays is needed for array_api_strict and dask.
103
+ d = np.asarray(d)[()]
104
+ if not np.isscalar(d) or (np.floor(d) != d):
105
+ raise ValueError("d should be a positive integer (dimension)")
106
+ if np.any(a <= 0.5 * (d - 1)):
107
+ raise ValueError(f"condition a ({a}) > 0.5 * (d-1) ({0.5 * (d-1)}) not met")
108
+
109
+ res = (d * (d-1) * 0.25) * np.log(np.pi)
110
+ res += np.sum(loggam([(a - (j - 1.)/2) for j in range(1, d+1)]), axis=0)
111
+ return res
112
+
113
+
114
+ _poisson_binom_pmf_doc = (
115
+ """Returns pmf of Poisson Binomial distribution.
116
+
117
+ Parameters
118
+ ----------
119
+ k : array
120
+ Number of successes at which to evaluate pmf.
121
+
122
+ p : array
123
+ Success probabilities of independent Bernoulli trials.
124
+
125
+ Notes
126
+ -----
127
+ This is equivalent to a gufunc with signature ``()(i)->()``.
128
+ The last dimension of `p` contains success probabilities and
129
+ the preceding dimensions are batch dimensions. The batch
130
+ dimensions are broadcast against ``k``.
131
+
132
+ The output will be C contiguous regardless of the contiguity of
133
+ `k` and `p`.
134
+
135
+ """
136
+ )
137
+
138
+
139
+ _poisson_binom_pmf = _with_cache_optimization(
140
+ name="_poisson_binom_pmf",
141
+ arg_names=["k", "p"],
142
+ docstring=_poisson_binom_pmf_doc,
143
+ ufunc=_gufuncs._poisson_binom_pmf,
144
+ cache_arg_indices=[1],
145
+ )
146
+
147
+
148
+ _poisson_binom_cdf_doc = (
149
+ """Returns cdf of Poisson Binomial distribution.
150
+
151
+ Parameters
152
+ ----------
153
+ k : array
154
+ Number of successes at which to evaluate cdf.
155
+
156
+ p : array
157
+ Success probabilities of independent Bernoulli trials.
158
+
159
+ Notes
160
+ -----
161
+ This is equivalent to a gufunc with signature ``()(i)->()``.
162
+ The last dimension of `p` contains success probabilities and
163
+ the preceding dimensions are batch dimensions. The batch
164
+ dimensions are broadcast against ``k``.
165
+
166
+ The output will be C contiguous regardless of the contiguity of
167
+ `k` and `p`.
168
+
169
+ """
170
+ )
171
+
172
+ _poisson_binom_cdf = _with_cache_optimization(
173
+ name="_poisson_binom_cdf",
174
+ arg_names=["k", "p"],
175
+ docstring=_poisson_binom_cdf_doc,
176
+ ufunc=_gufuncs._poisson_binom_cdf,
177
+ cache_arg_indices=[1],
178
+ )
v12x/pylib/scipy/special/_spherical_bessel.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import wraps
2
+ import scipy._external.array_api_extra as xpx
3
+ import numpy as np
4
+ from ._ufuncs import (_spherical_jn, _spherical_yn, _spherical_in,
5
+ _spherical_kn, _spherical_jn_d, _spherical_yn_d,
6
+ _spherical_in_d, _spherical_kn_d)
7
+
8
+
9
+ def use_reflection(sign_n_even=None, reflection_fun=None):
10
+ # - If reflection_fun is not specified, reflects negative `z` and multiplies
11
+ # output by appropriate sign (indicated by `sign_n_even`).
12
+ # - If reflection_fun is specified, calls `reflection_fun` instead of `fun`.
13
+ # See DLMF 10.47(v) https://dlmf.nist.gov/10.47
14
+ def decorator(fun):
15
+ def standard_reflection(n, z, derivative):
16
+ # sign_n_even indicates the sign when the order `n` is even
17
+ sign = np.where(n % 2 == 0, sign_n_even, -sign_n_even)
18
+ # By the chain rule, differentiation at `-z` adds a minus sign
19
+ sign = -sign if derivative else sign
20
+ # Evaluate at positive z (minus negative z) and adjust the sign
21
+ return fun(n, -z, derivative) * sign
22
+
23
+ @wraps(fun)
24
+ def wrapper(n, z, derivative=False):
25
+ z = np.asarray(z)
26
+
27
+ if np.issubdtype(z.dtype, np.complexfloating):
28
+ return fun(n, z, derivative) # complex dtype just works
29
+
30
+ f2 = standard_reflection if reflection_fun is None else reflection_fun
31
+ return xpx.apply_where(z.real >= 0, (n, z),
32
+ lambda n, z: fun(n, z, derivative),
33
+ lambda n, z: f2(n, z, derivative))[()]
34
+ return wrapper
35
+ return decorator
36
+
37
+
38
+ @use_reflection(+1) # See DLMF 10.47(v) https://dlmf.nist.gov/10.47
39
+ def spherical_jn(n, z, derivative=False):
40
+ r"""Spherical Bessel function of the first kind or its derivative.
41
+
42
+ Defined as [1]_,
43
+
44
+ .. math:: j_n(z) = \sqrt{\frac{\pi}{2z}} J_{n + 1/2}(z),
45
+
46
+ where :math:`J_n` is the Bessel function of the first kind.
47
+
48
+ Parameters
49
+ ----------
50
+ n : int, array_like
51
+ Order of the Bessel function (n >= 0).
52
+ z : complex or float, array_like
53
+ Argument of the Bessel function.
54
+ derivative : bool, optional
55
+ If True, the value of the derivative (rather than the function
56
+ itself) is returned.
57
+
58
+ Returns
59
+ -------
60
+ jn : ndarray
61
+ Value or derivative of spherical Bessel function of the first kind.
62
+
63
+ Notes
64
+ -----
65
+ For real arguments greater than the order, the function is computed
66
+ using the ascending recurrence [2]_. For small real or complex
67
+ arguments, the definitional relation to the cylindrical Bessel function
68
+ of the first kind is used.
69
+
70
+ The derivative is computed using the relations [3]_,
71
+
72
+ .. math::
73
+ j_n'(z) = j_{n-1}(z) - \frac{n + 1}{z} j_n(z).
74
+
75
+ j_0'(z) = -j_1(z)
76
+
77
+
78
+ .. versionadded:: 0.18.0
79
+
80
+ References
81
+ ----------
82
+ .. [1] https://dlmf.nist.gov/10.47.E3
83
+ .. [2] https://dlmf.nist.gov/10.51.E1
84
+ .. [3] https://dlmf.nist.gov/10.51.E2
85
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
86
+ Handbook of Mathematical Functions with Formulas,
87
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
88
+
89
+ Examples
90
+ --------
91
+ The spherical Bessel functions of the first kind :math:`j_n` accept
92
+ both real and complex second argument. They can return a complex type:
93
+
94
+ >>> from scipy.special import spherical_jn
95
+ >>> spherical_jn(0, 3+5j)
96
+ (-9.878987731663194-8.021894345786002j)
97
+ >>> type(spherical_jn(0, 3+5j))
98
+ <class 'numpy.complex128'>
99
+
100
+ We can verify the relation for the derivative from the Notes
101
+ for :math:`n=3` in the interval :math:`[1, 2]`:
102
+
103
+ >>> import numpy as np
104
+ >>> x = np.arange(1.0, 2.0, 0.01)
105
+ >>> np.allclose(spherical_jn(3, x, True),
106
+ ... spherical_jn(2, x) - 4/x * spherical_jn(3, x))
107
+ True
108
+
109
+ The first few :math:`j_n` with real argument:
110
+
111
+ >>> import matplotlib.pyplot as plt
112
+ >>> x = np.arange(0.0, 10.0, 0.01)
113
+ >>> fig, ax = plt.subplots()
114
+ >>> ax.set_ylim(-0.5, 1.5)
115
+ >>> ax.set_title(r'Spherical Bessel functions $j_n$')
116
+ >>> for n in np.arange(0, 4):
117
+ ... ax.plot(x, spherical_jn(n, x), label=rf'$j_{n}$')
118
+ >>> plt.legend(loc='best')
119
+ >>> plt.show()
120
+
121
+ """
122
+ n = np.asarray(n, dtype=np.dtype("long"))
123
+ if derivative:
124
+ return _spherical_jn_d(n, z)
125
+ else:
126
+ return _spherical_jn(n, z)
127
+
128
+
129
+ @use_reflection(-1) # See DLMF 10.47(v) https://dlmf.nist.gov/10.47
130
+ def spherical_yn(n, z, derivative=False):
131
+ r"""Spherical Bessel function of the second kind or its derivative.
132
+
133
+ Defined as [1]_,
134
+
135
+ .. math:: y_n(z) = \sqrt{\frac{\pi}{2z}} Y_{n + 1/2}(z),
136
+
137
+ where :math:`Y_n` is the Bessel function of the second kind.
138
+
139
+ Parameters
140
+ ----------
141
+ n : int, array_like
142
+ Order of the Bessel function (n >= 0).
143
+ z : complex or float, array_like
144
+ Argument of the Bessel function.
145
+ derivative : bool, optional
146
+ If True, the value of the derivative (rather than the function
147
+ itself) is returned.
148
+
149
+ Returns
150
+ -------
151
+ yn : ndarray
152
+ Value or derivative of spherical Bessel function of the second kind.
153
+
154
+ Notes
155
+ -----
156
+ For real arguments, the function is computed using the ascending
157
+ recurrence [2]_. For complex arguments, the definitional relation to
158
+ the cylindrical Bessel function of the second kind is used.
159
+
160
+ The derivative is computed using the relations [3]_,
161
+
162
+ .. math::
163
+ y_n' = y_{n-1} - \frac{n + 1}{z} y_n.
164
+
165
+ y_0' = -y_1
166
+
167
+
168
+ .. versionadded:: 0.18.0
169
+
170
+ References
171
+ ----------
172
+ .. [1] https://dlmf.nist.gov/10.47.E4
173
+ .. [2] https://dlmf.nist.gov/10.51.E1
174
+ .. [3] https://dlmf.nist.gov/10.51.E2
175
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
176
+ Handbook of Mathematical Functions with Formulas,
177
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
178
+
179
+ Examples
180
+ --------
181
+ The spherical Bessel functions of the second kind :math:`y_n` accept
182
+ both real and complex second argument. They can return a complex type:
183
+
184
+ >>> from scipy.special import spherical_yn
185
+ >>> spherical_yn(0, 3+5j)
186
+ (8.022343088587197-9.880052589376795j)
187
+ >>> type(spherical_yn(0, 3+5j))
188
+ <class 'numpy.complex128'>
189
+
190
+ We can verify the relation for the derivative from the Notes
191
+ for :math:`n=3` in the interval :math:`[1, 2]`:
192
+
193
+ >>> import numpy as np
194
+ >>> x = np.arange(1.0, 2.0, 0.01)
195
+ >>> np.allclose(spherical_yn(3, x, True),
196
+ ... spherical_yn(2, x) - 4/x * spherical_yn(3, x))
197
+ True
198
+
199
+ The first few :math:`y_n` with real argument:
200
+
201
+ >>> import matplotlib.pyplot as plt
202
+ >>> x = np.arange(0.0, 10.0, 0.01)
203
+ >>> fig, ax = plt.subplots()
204
+ >>> ax.set_ylim(-2.0, 1.0)
205
+ >>> ax.set_title(r'Spherical Bessel functions $y_n$')
206
+ >>> for n in np.arange(0, 4):
207
+ ... ax.plot(x, spherical_yn(n, x), label=rf'$y_{n}$')
208
+ >>> plt.legend(loc='best')
209
+ >>> plt.show()
210
+
211
+ """
212
+ n = np.asarray(n, dtype=np.dtype("long"))
213
+ if derivative:
214
+ return _spherical_yn_d(n, z)
215
+ else:
216
+ return _spherical_yn(n, z)
217
+
218
+
219
+ @use_reflection(+1) # See DLMF 10.47(v) https://dlmf.nist.gov/10.47
220
+ def spherical_in(n, z, derivative=False):
221
+ r"""Modified spherical Bessel function of the first kind or its derivative.
222
+
223
+ Defined as [1]_,
224
+
225
+ .. math:: i_n(z) = \sqrt{\frac{\pi}{2z}} I_{n + 1/2}(z),
226
+
227
+ where :math:`I_n` is the modified Bessel function of the first kind.
228
+
229
+ Parameters
230
+ ----------
231
+ n : int, array_like
232
+ Order of the Bessel function (n >= 0).
233
+ z : complex or float, array_like
234
+ Argument of the Bessel function.
235
+ derivative : bool, optional
236
+ If True, the value of the derivative (rather than the function
237
+ itself) is returned.
238
+
239
+ Returns
240
+ -------
241
+ in : ndarray
242
+ Value or derivative of modified spherical Bessel function of the first kind.
243
+
244
+ Notes
245
+ -----
246
+ The function is computed using its definitional relation to the
247
+ modified cylindrical Bessel function of the first kind.
248
+
249
+ The derivative is computed using the relations [2]_,
250
+
251
+ .. math::
252
+ i_n' = i_{n-1} - \frac{n + 1}{z} i_n.
253
+
254
+ i_1' = i_0
255
+
256
+
257
+ .. versionadded:: 0.18.0
258
+
259
+ References
260
+ ----------
261
+ .. [1] https://dlmf.nist.gov/10.47.E7
262
+ .. [2] https://dlmf.nist.gov/10.51.E5
263
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
264
+ Handbook of Mathematical Functions with Formulas,
265
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
266
+
267
+ Examples
268
+ --------
269
+ The modified spherical Bessel functions of the first kind :math:`i_n`
270
+ accept both real and complex second argument.
271
+ They can return a complex type:
272
+
273
+ >>> from scipy.special import spherical_in
274
+ >>> spherical_in(0, 3+5j)
275
+ (-1.1689867793369182-1.2697305267234222j)
276
+ >>> type(spherical_in(0, 3+5j))
277
+ <class 'numpy.complex128'>
278
+
279
+ We can verify the relation for the derivative from the Notes
280
+ for :math:`n=3` in the interval :math:`[1, 2]`:
281
+
282
+ >>> import numpy as np
283
+ >>> x = np.arange(1.0, 2.0, 0.01)
284
+ >>> np.allclose(spherical_in(3, x, True),
285
+ ... spherical_in(2, x) - 4/x * spherical_in(3, x))
286
+ True
287
+
288
+ The first few :math:`i_n` with real argument:
289
+
290
+ >>> import matplotlib.pyplot as plt
291
+ >>> x = np.arange(0.0, 6.0, 0.01)
292
+ >>> fig, ax = plt.subplots()
293
+ >>> ax.set_ylim(-0.5, 5.0)
294
+ >>> ax.set_title(r'Modified spherical Bessel functions $i_n$')
295
+ >>> for n in np.arange(0, 4):
296
+ ... ax.plot(x, spherical_in(n, x), label=rf'$i_{n}$')
297
+ >>> plt.legend(loc='best')
298
+ >>> plt.show()
299
+
300
+ """
301
+ n = np.asarray(n, dtype=np.dtype("long"))
302
+ if derivative:
303
+ return _spherical_in_d(n, z)
304
+ else:
305
+ return _spherical_in(n, z)
306
+
307
+
308
+ def spherical_kn_reflection(n, z, derivative=False):
309
+ # More complex than the other cases, and this will likely be re-implemented
310
+ # in C++ anyway. Would require multiple function evaluations. Probably about
311
+ # as fast to just resort to complex math, and much simpler.
312
+ return spherical_kn(n, z + 0j, derivative=derivative).real
313
+
314
+
315
+ @use_reflection(reflection_fun=spherical_kn_reflection)
316
+ def spherical_kn(n, z, derivative=False):
317
+ r"""Modified spherical Bessel function of the second kind or its derivative.
318
+
319
+ Defined as [1]_,
320
+
321
+ .. math:: k_n(z) = \sqrt{\frac{\pi}{2z}} K_{n + 1/2}(z),
322
+
323
+ where :math:`K_n` is the modified Bessel function of the second kind.
324
+
325
+ Parameters
326
+ ----------
327
+ n : int, array_like
328
+ Order of the Bessel function (n >= 0).
329
+ z : complex or float, array_like
330
+ Argument of the Bessel function.
331
+ derivative : bool, optional
332
+ If True, the value of the derivative (rather than the function
333
+ itself) is returned.
334
+
335
+ Returns
336
+ -------
337
+ kn : ndarray
338
+ Value or derivative of modified spherical Bessel function of the second kind.
339
+
340
+ Notes
341
+ -----
342
+ The function is computed using its definitional relation to the
343
+ modified cylindrical Bessel function of the second kind.
344
+
345
+ The derivative is computed using the relations [2]_,
346
+
347
+ .. math::
348
+ k_n' = -k_{n-1} - \frac{n + 1}{z} k_n.
349
+
350
+ k_0' = -k_1
351
+
352
+
353
+ .. versionadded:: 0.18.0
354
+
355
+ References
356
+ ----------
357
+ .. [1] https://dlmf.nist.gov/10.47.E9
358
+ .. [2] https://dlmf.nist.gov/10.51.E5
359
+ .. [AS] Milton Abramowitz and Irene A. Stegun, eds.
360
+ Handbook of Mathematical Functions with Formulas,
361
+ Graphs, and Mathematical Tables. New York: Dover, 1972.
362
+
363
+ Examples
364
+ --------
365
+ The modified spherical Bessel functions of the second kind :math:`k_n`
366
+ accept both real and complex second argument.
367
+ They can return a complex type:
368
+
369
+ >>> from scipy.special import spherical_kn
370
+ >>> spherical_kn(0, 3+5j)
371
+ (0.012985785614001561+0.003354691603137546j)
372
+ >>> type(spherical_kn(0, 3+5j))
373
+ <class 'numpy.complex128'>
374
+
375
+ We can verify the relation for the derivative from the Notes
376
+ for :math:`n=3` in the interval :math:`[1, 2]`:
377
+
378
+ >>> import numpy as np
379
+ >>> x = np.arange(1.0, 2.0, 0.01)
380
+ >>> np.allclose(spherical_kn(3, x, True),
381
+ ... - 4/x * spherical_kn(3, x) - spherical_kn(2, x))
382
+ True
383
+
384
+ The first few :math:`k_n` with real argument:
385
+
386
+ >>> import matplotlib.pyplot as plt
387
+ >>> x = np.arange(0.0, 4.0, 0.01)
388
+ >>> fig, ax = plt.subplots()
389
+ >>> ax.set_ylim(0.0, 5.0)
390
+ >>> ax.set_title(r'Modified spherical Bessel functions $k_n$')
391
+ >>> for n in np.arange(0, 4):
392
+ ... ax.plot(x, spherical_kn(n, x), label=rf'$k_{n}$')
393
+ >>> plt.legend(loc='best')
394
+ >>> plt.show()
395
+
396
+ """
397
+ n = np.asarray(n, dtype=np.dtype("long"))
398
+ if derivative:
399
+ return _spherical_kn_d(n, z)
400
+ else:
401
+ return _spherical_kn(n, z)
v12x/pylib/scipy/special/_support_alternative_backends.py ADDED
@@ -0,0 +1,933 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ import operator
3
+ from collections.abc import Callable, Mapping
4
+ from dataclasses import dataclass
5
+ from types import ModuleType
6
+
7
+ import numpy as np
8
+ from scipy._lib._array_api import (
9
+ array_namespace, scipy_namespace_for, is_numpy, is_dask, is_marray, is_jax_array,
10
+ is_jax, xp_promote, xp_capabilities, SCIPY_ARRAY_API, get_native_namespace_name,
11
+ is_array_api_obj
12
+ )
13
+ import scipy._external.array_api_extra as xpx
14
+ from . import _basic
15
+ from . import _spfun_stats
16
+ from . import _ufuncs
17
+
18
+ # mypy: disable-error-code=dict-item
19
+
20
+ def _special_namespace_for(xp):
21
+ spx = scipy_namespace_for(xp)
22
+ return getattr(spx, "special", None)
23
+
24
+
25
+ @dataclass
26
+ class _FuncInfo:
27
+ # NumPy-only function. IT MUST BE ELEMENTWISE.
28
+ func: Callable
29
+ # Number of arguments, not counting out=
30
+ # This is for testing purposes only, due to the fact that
31
+ # inspect.signature() just returns *args for ufuncs.
32
+ n_args: int
33
+ # @xp_capabilities decorator, for the purpose of
34
+ # documentation and unit testing. Omit to indicate
35
+ # full support for all backends.
36
+ xp_capabilities: Callable[[Callable], Callable] | None = None
37
+ # Generic implementation to fall back on if there is no native dispatch
38
+ # available. This is a function that accepts (main namespace, scipy namespace)
39
+ # and returns the final callable, or None if not available.
40
+ generic_impl: Callable[
41
+ [ModuleType, ModuleType | None], Callable | None
42
+ ] | None = None
43
+ # Handle case where a backend uses an alternative name for a function.
44
+ # Should map backend names to alternative function names.
45
+ alt_names_map: dict[str, str] | None = None
46
+ # Some functions only take integer arrays for some arguments.
47
+ int_only: tuple[bool, ...] | None = None
48
+ # For testing purposes, whether tests should only use positive values
49
+ # for some arguments. If bool and equal to True, restrict to positive
50
+ # values for all arguments. To restrict only some arguments to positive
51
+ # values, pass a tuple of bool of the same length as the number of
52
+ # arguments, the ith entry in the tuple controls positive_only for
53
+ # the ith argument. To make backend specific choices for positive_only,
54
+ # pass in a dict mapping backend names to bool or tuple[bool, ...].
55
+ positive_only: (
56
+ bool | tuple[bool, ...] | Mapping[str, bool | tuple[bool, ...]]
57
+ ) = False
58
+ # Some special functions are not ufuncs and ufunc-specific tests
59
+ # should not be applied to these.
60
+ is_ufunc: bool = True
61
+ # Some non-ufunc special functions take only Python ints for some arguments.
62
+ # If so, python_int_only should be a tuple of the same length as the number
63
+ # of arguments,with value True if the corresponding argument needs to be a
64
+ # Python int.
65
+ # Can also take a dict mapping backends to such tuples if an argument being
66
+ # Python int only is backend specific.
67
+ python_int_only: dict[str, tuple[bool, ...]] | tuple[bool, ...] | None = None
68
+ # Some functions which seem to be scalar also accept 0d arrays.
69
+ scalar_or_0d_only: dict[str, tuple[bool, ...]] | tuple[bool, ...] | None = None
70
+ # Some functions may not work well with very large integer valued arguments.
71
+ test_large_ints: bool = True
72
+ # Some non-ufunc special functions don't decay 0d arrays to scalar.
73
+ produces_0d: bool = False
74
+ # Whether or not uses native PyTorch or falls back to NumPy/SciPy. This
75
+ # is needed because in PyTorch, the default dtype affects promotion
76
+ # rules when mixing integer and floating dtypes, so relying on a
77
+ # NumPy/SciPy fallback when the default dtype is other than float64 can lead
78
+ # to float64 output when native PyTorch would have e.g. float32 output. This
79
+ # must be accounted for in tests. Not putting this in xp_capabilities for now,
80
+ # but in the future I think it's likely we may want to add a warning to
81
+ # xp_capabilities when not using native PyTorch on CPU.
82
+ torch_native: bool = True
83
+ # Place a backend in this tuple if `func` is available as `xp.func` but not
84
+ # available in the `scipy.special` namespace for this backend.
85
+ # One example is `jax.numpy.sinc` being available but not `jax.scipy.special.sinc`.
86
+ backends_with_func_in_xp: tuple[str, ...] = ()
87
+
88
+ @property
89
+ def name(self):
90
+ return self.func.__name__
91
+
92
+ # These are needed by @lru_cache below
93
+ def __hash__(self):
94
+ return hash(self.func)
95
+
96
+ def __eq__(self, other):
97
+ return isinstance(other, _FuncInfo) and self.func == other.func
98
+
99
+ @property
100
+ def wrapper(self):
101
+ if self.name in globals():
102
+ # Already initialised. We are likely in a unit test.
103
+ # Return function potentially overridden by xpx.testing.lazy_xp_function.
104
+ import scipy.special
105
+ return getattr(scipy.special, self.name)
106
+
107
+ if SCIPY_ARRAY_API:
108
+ @functools.wraps(self.func)
109
+ def wrapped(*args, **kwargs):
110
+ xp = array_namespace(*args)
111
+ return self._wrapper_for(xp)(*args, **kwargs)
112
+
113
+ # Allow pickling the function. Normally this is done by @wraps,
114
+ # but in this case it doesn't work because self.func is a ufunc.
115
+ wrapped.__module__ = "scipy.special"
116
+ wrapped.__qualname__ = self.name
117
+ func = wrapped
118
+ else:
119
+ func = self.func
120
+
121
+ capabilities = self.xp_capabilities or xp_capabilities()
122
+ # In order to retain a naked ufunc when SCIPY_ARRAY_API is
123
+ # disabled, xp_capabilities must apply its changes in place.
124
+ cap_func = capabilities(func)
125
+ assert cap_func is func
126
+ return func
127
+
128
+ @functools.lru_cache(1000)
129
+ def _wrapper_for(self, xp):
130
+ if is_numpy(xp):
131
+ return self.func
132
+
133
+ # If a native implementation is available, use that
134
+ in_xp = get_native_namespace_name(xp) in self.backends_with_func_in_xp
135
+ namespace = xp if in_xp else _special_namespace_for(xp)
136
+ f = _get_native_func(
137
+ xp, namespace, self.name, alt_names_map=self.alt_names_map
138
+ )
139
+ if f is not None:
140
+ return f
141
+
142
+ if in_xp:
143
+ # when namespace is passed to self.generic_impl below, we want to
144
+ # make sure that it is the special namespace for xp and not xp
145
+ # itself, so raise if xp was incorrectly placed in
146
+ # `backends_with_func_in_xp`.
147
+ raise RuntimeError(
148
+ f"func {self.func} is not available as {xp.__name__}.{self.func}"
149
+ f" but {xp.__name__} was passed in ``in_xp``."
150
+ )
151
+
152
+ # If generic Array API implementation is available, use that
153
+ if self.generic_impl is not None:
154
+ f = self.generic_impl(xp, namespace)
155
+ if f is not None:
156
+ return f
157
+
158
+ if is_marray(xp):
159
+ # Unwrap the array, apply the function on the wrapped namespace,
160
+ # and then re-wrap it.
161
+ # IMPORTANT: this only works because all functions in this module
162
+ # are elementwise. Otherwise, we would not be able to define a
163
+ # general rule for mask propagation.
164
+
165
+ _f = globals()[self.name] # Allow nested wrapping
166
+ def f(*args, _f=_f, xp=xp, **kwargs):
167
+ data_args = [getattr(arg, 'data', arg) for arg in args]
168
+ out = _f(*data_args, **kwargs)
169
+ mask = functools.reduce(operator.or_,
170
+ (getattr(arg, 'mask', False) for arg in args))
171
+ return xp.asarray(out, mask=mask)
172
+
173
+ return f
174
+
175
+ if is_dask(xp):
176
+ # Apply the function to each block of the Dask array.
177
+ # IMPORTANT: map_blocks works only because all functions in this module
178
+ # are elementwise. It would be a grave mistake to apply this to gufuncs
179
+ # or any other function with reductions, as they would change their
180
+ # output depending on chunking!
181
+
182
+ _f = globals()[self.name] # Allow nested wrapping
183
+ def f(*args, _f=_f, xp=xp, **kwargs):
184
+ # Hide dtype kwarg from map_blocks
185
+ return xp.map_blocks(functools.partial(_f, **kwargs), *args)
186
+
187
+ return f
188
+
189
+ # As a final resort, use the NumPy/SciPy implementation
190
+ _f = self.func
191
+
192
+ if is_jax(xp) and self.is_ufunc:
193
+ # if this is a ufunc, we can use the resolve_dtypes method to figure
194
+ # out what the output dtype should be and use lazy_apply to make this
195
+ # work with the JAX JIT. Since pure_callback used in lazy_apply will
196
+ # convert all of the inputs to eager JAX arrays, we will also need to
197
+ # get the input dtypes inferred from resolve_dtypes so that Python
198
+ # scalar inputs can be cast to the correct dtype under NEP50 weak
199
+ # promotion rules rather than getting promoted to the default currently
200
+ # set in JAX. One cannot just use xp_promote for the input dtypes because
201
+ # some ufuncs have integer only args.
202
+ def f(*args, _f=_f, xp=xp, **kwargs):
203
+ dtypes = (arg.dtype if is_jax_array(arg) else type(arg) for arg in args)
204
+ # result_dtypes needs an arg for the dtype of the optional out param.
205
+ # Use `None` since `out` is incompatible with JAX's immutability.
206
+ dtypes = (*dtypes, None)
207
+ # JAX uses NumPy dtypes so we can just pass these directly to
208
+ # resolve_dtypes. TODO: generalize to other lazy backends.
209
+ dtypes = _f.resolve_dtypes(dtypes)
210
+ out_dtype = dtypes[-1]
211
+ args = [
212
+ xp.asarray(arg, dtype=dtype)
213
+ for arg, dtype in zip(args, dtypes[:-1])
214
+ ]
215
+ return xpx.lazy_apply(
216
+ _f, *args, xp=xp, as_numpy=True, dtype=out_dtype, **kwargs
217
+ )
218
+ else:
219
+ def f(*args, _f=_f, xp=xp, **kwargs):
220
+ # Check with `is_array_api_obj` to keep Python scalars untouched so that
221
+ # NEP50 can be followed.
222
+ args = [
223
+ np.asarray(arg) if is_array_api_obj(arg) else arg for arg in args
224
+ ]
225
+ out = _f(*args, **kwargs)
226
+ return xp.asarray(out)
227
+
228
+ return f
229
+
230
+
231
+ def _get_native_func(xp, namespace, f_name, *, alt_names_map=None):
232
+ if alt_names_map is None:
233
+ alt_names_map = {}
234
+ f_name = alt_names_map.get(get_native_namespace_name(xp), f_name)
235
+ f = getattr(namespace, f_name, None)
236
+ if f is None and hasattr(xp, 'special'):
237
+ # Currently dead branch, in anticipation of 'special' Array API extension
238
+ # https://github.com/data-apis/array-api/issues/725
239
+ f = getattr(xp.special, f_name, None)
240
+ return f
241
+
242
+
243
+ def _rel_entr(xp, spsx):
244
+ def __rel_entr(x, y, *, xp=xp):
245
+ # https://github.com/data-apis/array-api-extra/issues/160
246
+ mxp = array_namespace(x._meta, y._meta) if is_dask(xp) else xp
247
+ x, y = xp_promote(x, y, broadcast=True, force_floating=True, xp=xp)
248
+ xy_pos = (x > 0) & (y > 0)
249
+ xy_inf = xp.isinf(x) & xp.isinf(y)
250
+ res = xpx.apply_where(
251
+ xy_pos & ~xy_inf,
252
+ (x, y),
253
+ # Note: for very large x, this can overflow.
254
+ lambda x, y: x * (mxp.log(x) - mxp.log(y)),
255
+ fill_value=xp.inf
256
+ )
257
+ res = xpx.at(res)[(x == 0) & (y >= 0)].set(0)
258
+ res = xpx.at(res)[xp.isnan(x) | xp.isnan(y) | (xy_pos & xy_inf)].set(xp.nan)
259
+ return res
260
+
261
+ return __rel_entr
262
+
263
+
264
+ def _xlogy(xp, spsx):
265
+ def __xlogy(x, y, *, xp=xp):
266
+ x, y = xp_promote(x, y, force_floating=True, xp=xp)
267
+ with np.errstate(divide='ignore', invalid='ignore'):
268
+ temp = x * xp.log(y)
269
+ return xp.where(x == 0., 0., temp)
270
+ return __xlogy
271
+
272
+
273
+
274
+ def _chdtr(xp, spsx):
275
+ # The difference between this and just using `gammainc`
276
+ # defined by `get_array_special_func` is that if `gammainc`
277
+ # isn't found, we don't want to use the SciPy version; we'll
278
+ # return None here and use the SciPy version of `chdtr`.
279
+ gammainc = _get_native_func(xp, spsx, 'gammainc')
280
+ if gammainc is None:
281
+ return None
282
+
283
+ def __chdtr(v, x):
284
+ res = gammainc(v / 2, x / 2) # this is almost all we need
285
+ # The rest can be removed when google/jax#20507 is resolved
286
+ mask = (v == 0) & (x > 0) # JAX returns NaN
287
+ res = xp.where(mask, 1., res)
288
+ mask = xp.isinf(v) & xp.isinf(x) # JAX returns 1.0
289
+ return xp.where(mask, xp.nan, res)
290
+ return __chdtr
291
+
292
+
293
+ def _chdtrc(xp, spsx):
294
+ # The difference between this and just using `gammaincc`
295
+ # defined by `get_array_special_func` is that if `gammaincc`
296
+ # isn't found, we don't want to use the SciPy version; we'll
297
+ # return None here and use the SciPy version of `chdtrc`.
298
+ gammaincc = _get_native_func(xp, spsx, 'gammaincc')
299
+ if gammaincc is None:
300
+ return None
301
+
302
+ def __chdtrc(v, x):
303
+ res = xp.where(x >= 0, gammaincc(v/2, x/2), 1)
304
+ i_nan = ((x == 0) & (v == 0)) | xp.isnan(x) | xp.isnan(v) | (v < 0)
305
+ res = xp.where(i_nan, xp.nan, res)
306
+ return res
307
+ return __chdtrc
308
+
309
+
310
+ def _betaincc(xp, spsx):
311
+ betainc = _get_native_func(xp, spsx, 'betainc')
312
+ if betainc is None:
313
+ return None
314
+
315
+ def __betaincc(a, b, x):
316
+ # not perfect; might want to just rely on SciPy
317
+ return betainc(b, a, 1-x)
318
+ return __betaincc
319
+
320
+
321
+ def _stdtr(xp, spsx):
322
+ betainc = _get_native_func(xp, spsx, 'betainc')
323
+ if betainc is None:
324
+ return None
325
+
326
+ def __stdtr(df, t):
327
+ x = df / (t ** 2 + df)
328
+ tail = betainc(df / 2, 0.5, x) / 2
329
+ return xp.where(t < 0, tail, 1 - tail)
330
+
331
+ return __stdtr
332
+
333
+
334
+ def _stdtrit(xp, spsx):
335
+ # Need either native stdtr or native betainc
336
+ stdtr = _get_native_func(xp, spsx, 'stdtr') or _stdtr(xp, spsx)
337
+ # If betainc is not defined, the root-finding would be done with `xp`
338
+ # despite `stdtr` being evaluated with SciPy/NumPy `stdtr`. Save the
339
+ # conversions: in this case, just evaluate `stdtrit` with SciPy/NumPy.
340
+ if stdtr is None:
341
+ return None
342
+
343
+ from scipy.optimize.elementwise import bracket_root, find_root
344
+
345
+ def __stdtrit(df, p):
346
+ def fun(t, df, p): return stdtr(df, t) - p
347
+ res_bracket = bracket_root(fun, xp.zeros_like(p), args=(df, p))
348
+ res_root = find_root(fun, res_bracket.bracket, args=(df, p))
349
+ return res_root.x
350
+
351
+ return __stdtrit
352
+
353
+
354
+ # Inventory of automatically dispatched functions
355
+ # IMPORTANT: these must all be **elementwise** functions!
356
+
357
+ # PyTorch doesn't implement `betainc`.
358
+ # On torch CPU we can fall back to NumPy, but on GPU it won't work.
359
+ _needs_betainc = xp_capabilities(cpu_only=True, exceptions=["jax.numpy", "cupy"])
360
+
361
+ _special_funcs = (
362
+ _FuncInfo(
363
+ _ufuncs.bdtr, 3,
364
+ xp_capabilities(
365
+ cpu_only=True, exceptions=["cupy"],
366
+ jax_jit=True,
367
+ ),
368
+ int_only=(False, True, False), torch_native=False,
369
+ ),
370
+ _FuncInfo(
371
+ _ufuncs.bdtrc, 3,
372
+ xp_capabilities(
373
+ cpu_only=True, exceptions=["cupy"],
374
+ jax_jit=True,
375
+ ),
376
+ int_only=(False, True, False), torch_native=False,
377
+ ),
378
+ _FuncInfo(
379
+ _ufuncs.bdtri, 3,
380
+ xp_capabilities(
381
+ cpu_only=True, exceptions=["cupy"],
382
+ jax_jit=True,
383
+ ),
384
+ int_only=(False, True, False), torch_native=False,
385
+ ),
386
+ _FuncInfo(_ufuncs.betainc, 3, _needs_betainc, torch_native=False),
387
+ _FuncInfo(_ufuncs.betaincc, 3, _needs_betainc, generic_impl=_betaincc,
388
+ torch_native=False),
389
+ _FuncInfo(
390
+ _ufuncs.betaincinv, 3,
391
+ xp_capabilities(
392
+ cpu_only=True, exceptions=["cupy"],
393
+ jax_jit=True,
394
+ ),
395
+ test_large_ints=False, positive_only=True, torch_native=False,
396
+ ),
397
+ _FuncInfo(
398
+ _ufuncs.betaln, 2,
399
+ xp_capabilities(cpu_only=True, exceptions=["cupy", "jax.numpy"]),
400
+ # For betaln, nan mismatches can occur at negative integer a or b of
401
+ # sufficiently large magnitude.
402
+ positive_only={"jax.numpy": True}, torch_native=False,
403
+ ),
404
+ _FuncInfo(
405
+ _ufuncs.binom, 2,
406
+ xp_capabilities(
407
+ cpu_only=True, exceptions=["cupy"],
408
+ jax_jit=True,
409
+ ),
410
+ torch_native=False,
411
+ ),
412
+ _FuncInfo(
413
+ _ufuncs.boxcox, 2,
414
+ xp_capabilities(
415
+ cpu_only=True, exceptions=["cupy"],
416
+ jax_jit=True,
417
+ ),
418
+ torch_native=False,
419
+ ),
420
+ _FuncInfo(
421
+ _ufuncs.boxcox1p, 2,
422
+ xp_capabilities(
423
+ cpu_only=True, exceptions=["cupy"],
424
+ jax_jit=True,
425
+ ),
426
+ torch_native=False,
427
+ ),
428
+ _FuncInfo(
429
+ _ufuncs.cbrt, 1,
430
+ xp_capabilities(
431
+ cpu_only=True, exceptions=["cupy"],
432
+ jax_jit=True,
433
+ ),
434
+ torch_native=False,
435
+ ),
436
+ _FuncInfo(_ufuncs.chdtr, 2, generic_impl=_chdtr),
437
+ _FuncInfo(_ufuncs.chdtrc, 2, generic_impl=_chdtrc,
438
+ # scipy/scipy#20972
439
+ positive_only={"cupy": True, "jax.numpy": True, "torch": True}),
440
+ _FuncInfo(
441
+ _ufuncs.chdtri, 2,
442
+ xp_capabilities(
443
+ cpu_only=True, exceptions=["cupy"],
444
+ jax_jit=True,
445
+ ),
446
+ torch_native=False,
447
+ ),
448
+ _FuncInfo(
449
+ _ufuncs.cosdg, 1,
450
+ xp_capabilities(
451
+ cpu_only=True, exceptions=["cupy"],
452
+ jax_jit=True,
453
+ ),
454
+ test_large_ints=False, torch_native=False,
455
+ ),
456
+ _FuncInfo(
457
+ _ufuncs.cosm1, 1,
458
+ xp_capabilities(
459
+ cpu_only=True, exceptions=["cupy"],
460
+ jax_jit=True,
461
+ ),
462
+ torch_native=False,
463
+ ),
464
+ _FuncInfo(
465
+ _ufuncs.cotdg, 1,
466
+ xp_capabilities(
467
+ cpu_only=True, exceptions=["cupy"],
468
+ jax_jit=True,
469
+ ),
470
+ torch_native=False,
471
+ ),
472
+ _FuncInfo(
473
+ _ufuncs.ellipk, 1,
474
+ xp_capabilities(
475
+ cpu_only=True, exceptions=["cupy"],
476
+ jax_jit=True,
477
+ ),
478
+ torch_native=False,
479
+ ),
480
+ _FuncInfo(
481
+ _ufuncs.ellipkm1, 1,
482
+ xp_capabilities(
483
+ cpu_only=True, exceptions=["cupy"],
484
+ jax_jit=True,
485
+ ),
486
+ torch_native=False,
487
+ ),
488
+ _FuncInfo(_ufuncs.entr, 1),
489
+ _FuncInfo(_ufuncs.erf, 1),
490
+ _FuncInfo(_ufuncs.erfc, 1),
491
+ _FuncInfo(
492
+ _ufuncs.erfcx, 1,
493
+ xp_capabilities(
494
+ cpu_only=True, exceptions=["cupy", "torch"],
495
+ jax_jit=True,
496
+ ),
497
+ torch_native=False,
498
+ ),
499
+ _FuncInfo(_ufuncs.erfinv, 1),
500
+ _FuncInfo(
501
+ _ufuncs.exp1, 1,
502
+ xp_capabilities(
503
+ cpu_only=True, exceptions=["cupy"],
504
+ jax_jit=True,
505
+ ),
506
+ torch_native=False,
507
+ ),
508
+ _FuncInfo(
509
+ _ufuncs.exp10, 1,
510
+ xp_capabilities(
511
+ cpu_only=True, exceptions=["cupy"],
512
+ jax_jit=True,
513
+ ),
514
+ torch_native=False,
515
+ ),
516
+ _FuncInfo(
517
+ _ufuncs.exp2, 1,
518
+ xp_capabilities(
519
+ cpu_only=True, exceptions=["cupy"],
520
+ jax_jit=True,
521
+ ),
522
+ torch_native=False,
523
+ ),
524
+ _FuncInfo(
525
+ _ufuncs.exprel, 1,
526
+ xp_capabilities(
527
+ cpu_only=True, exceptions=["cupy"],
528
+ jax_jit=True,
529
+ ),
530
+ torch_native=False,
531
+ ),
532
+ _FuncInfo(
533
+ _ufuncs.expi, 1,
534
+ xp_capabilities(cpu_only=True, exceptions=["cupy", "jax.numpy"]),
535
+ torch_native=False,
536
+ ),
537
+ _FuncInfo(_ufuncs.expit, 1),
538
+ _FuncInfo(
539
+ _ufuncs.expn, 2,
540
+ xp_capabilities(cpu_only=True, exceptions=["cupy", "jax.numpy"]),
541
+ # Inconsistent behavior for negative n. expn is not defined here without
542
+ # taking analytic continuation.
543
+ positive_only=True,
544
+ int_only=(True, False), test_large_ints=False,
545
+ torch_native=False,
546
+ ),
547
+ _FuncInfo(
548
+ _ufuncs.fdtr, 3,
549
+ xp_capabilities(
550
+ cpu_only=True, exceptions=["cupy"],
551
+ jax_jit=True,
552
+ ),
553
+ torch_native=False,
554
+ ),
555
+ _FuncInfo(
556
+ _ufuncs.fdtrc, 3,
557
+ xp_capabilities(
558
+ cpu_only=True, exceptions=["cupy"],
559
+ jax_jit=True,
560
+ ),
561
+ torch_native=False,
562
+ ),
563
+ _FuncInfo(
564
+ _ufuncs.fdtri, 3,
565
+ xp_capabilities(
566
+ cpu_only=True, exceptions=["cupy"],
567
+ jax_jit=True,
568
+ ),
569
+ torch_native=False,
570
+ ),
571
+ _FuncInfo(
572
+ _ufuncs.gamma, 1,
573
+ xp_capabilities(cpu_only=True, exceptions=["cupy", "jax.numpy"]),
574
+ torch_native=False,
575
+ ),
576
+ _FuncInfo(_ufuncs.gammainc, 2),
577
+ _FuncInfo(
578
+ _ufuncs.gammaincc, 2,
579
+ # google/jax#20699
580
+ positive_only={"jax.numpy": True},
581
+ ),
582
+ _FuncInfo(
583
+ _ufuncs.gammainccinv, 2,
584
+ xp_capabilities(
585
+ cpu_only=True, exceptions=["cupy"],
586
+ jax_jit=True,
587
+ ),
588
+ torch_native=False,
589
+ ),
590
+ _FuncInfo(
591
+ _ufuncs.gammaincinv, 2,
592
+ xp_capabilities(
593
+ cpu_only=True, exceptions=["cupy"],
594
+ jax_jit=True,
595
+ ),
596
+ torch_native=False,
597
+ ),
598
+ _FuncInfo(_ufuncs.gammaln, 1),
599
+ _FuncInfo(
600
+ _ufuncs.gammasgn, 1,
601
+ xp_capabilities(cpu_only=True, exceptions=["cupy", "jax.numpy"]),
602
+ torch_native=False,
603
+ ),
604
+ _FuncInfo(
605
+ _ufuncs.gdtr, 3,
606
+ xp_capabilities(
607
+ cpu_only=True, exceptions=["cupy"],
608
+ jax_jit=True,
609
+ ),
610
+ torch_native=False,
611
+ ),
612
+ _FuncInfo(
613
+ _ufuncs.gdtrc, 3,
614
+ xp_capabilities(
615
+ cpu_only=True, exceptions=["cupy"],
616
+ jax_jit=True,
617
+ ),
618
+ torch_native=False,
619
+ ),
620
+ _FuncInfo(
621
+ _ufuncs.huber, 2,
622
+ xp_capabilities(
623
+ cpu_only=True, exceptions=["cupy"],
624
+ jax_jit=True,
625
+ ),
626
+ torch_native=False,
627
+ ),
628
+ _FuncInfo(
629
+ _ufuncs.hyp1f1, 3,
630
+ xp_capabilities(cpu_only=True, exceptions=["jax.numpy"]),
631
+ positive_only={"jax.numpy": True}, test_large_ints=False,
632
+ torch_native=False,
633
+ ),
634
+ # Comment out when jax>=0.6.1 is available in Conda for CI.
635
+ # (or add version requirements to xp_capabilities).
636
+ # _FuncInfo(
637
+ # _ufuncs.hyp2f1, 4,
638
+ # xp_capabilities(cpu_only=True, exceptions=["jax.numpy"]),
639
+ # positive_only={"jax.numpy": True}, test_large_ints=False,
640
+ # torch_native=False,
641
+ # ),
642
+ _FuncInfo(
643
+ _ufuncs.inv_boxcox, 2,
644
+ xp_capabilities(
645
+ cpu_only=True, exceptions=["cupy"],
646
+ jax_jit=True,
647
+ ),
648
+ torch_native=False,
649
+ ),
650
+ _FuncInfo(
651
+ _ufuncs.inv_boxcox1p, 2,
652
+ xp_capabilities(
653
+ cpu_only=True, exceptions=["cupy"],
654
+ jax_jit=True,
655
+ ),
656
+ torch_native=False,
657
+ ),
658
+ _FuncInfo(_ufuncs.i0, 1),
659
+ _FuncInfo(_ufuncs.i0e, 1),
660
+ _FuncInfo(_ufuncs.i1, 1),
661
+ _FuncInfo(_ufuncs.i1e, 1),
662
+ _FuncInfo(
663
+ _ufuncs.j0, 1,
664
+ xp_capabilities(
665
+ cpu_only=True, exceptions=["cupy", "torch"],
666
+ jax_jit=True,
667
+ ),
668
+ alt_names_map={"torch": "bessel_j0"}, test_large_ints=False,
669
+ ),
670
+ _FuncInfo(
671
+ _ufuncs.j1, 1,
672
+ xp_capabilities(
673
+ cpu_only=True, exceptions=["cupy", "torch"],
674
+ jax_jit=True,
675
+ ),
676
+ alt_names_map={"torch": "bessel_j1"}, test_large_ints=False,
677
+ ),
678
+ _FuncInfo(
679
+ _ufuncs.k0, 1,
680
+ xp_capabilities(
681
+ cpu_only=True, exceptions=["cupy", "torch"],
682
+ jax_jit=True,
683
+ ),
684
+ alt_names_map={"torch": "modified_bessel_k0"},
685
+ ),
686
+ _FuncInfo(
687
+ _ufuncs.k0e, 1,
688
+ xp_capabilities(
689
+ cpu_only=True, exceptions=["cupy", "torch"],
690
+ jax_jit=True,
691
+ ),
692
+ alt_names_map={"torch": "scaled_modified_bessel_k0"},
693
+ test_large_ints=False,
694
+ ),
695
+ _FuncInfo(
696
+ _ufuncs.k1, 1,
697
+ xp_capabilities(
698
+ cpu_only=True, exceptions=["cupy", "torch"],
699
+ jax_jit=True,
700
+ ),
701
+ alt_names_map={"torch": "modified_bessel_k1"},
702
+ ),
703
+ _FuncInfo(
704
+ _ufuncs.k1e, 1,
705
+ xp_capabilities(
706
+ cpu_only=True, exceptions=["cupy", "torch"],
707
+ jax_jit=True,
708
+ ),
709
+ alt_names_map={"torch": "scaled_modified_bessel_k1"},
710
+ test_large_ints=False),
711
+ _FuncInfo(
712
+ _ufuncs.kl_div, 2,
713
+ xp_capabilities(cpu_only=True, exceptions=["cupy", "jax.numpy"]),
714
+ torch_native=False,
715
+ ),
716
+ _FuncInfo(_ufuncs.log_ndtr, 1),
717
+ _FuncInfo(
718
+ _ufuncs.loggamma, 1,
719
+ xp_capabilities(
720
+ cpu_only=True, exceptions=["cupy"],
721
+ jax_jit=True,
722
+ ),
723
+ torch_native=False,
724
+ ),
725
+ _FuncInfo(_ufuncs.logit, 1),
726
+ _FuncInfo(
727
+ _ufuncs.lpmv, 3,
728
+ xp_capabilities(
729
+ cpu_only=True, exceptions=["cupy"],
730
+ jax_jit=True,
731
+ ),
732
+ torch_native=False,
733
+ test_large_ints=False,
734
+ ),
735
+ _FuncInfo(
736
+ _spfun_stats.multigammaln, 2,
737
+ is_ufunc=False,
738
+ python_int_only={
739
+ "cupy": (False, True),
740
+ "jax.numpy": (False, True),
741
+ "torch": (False, True),
742
+ },
743
+ scalar_or_0d_only={
744
+ "array_api_strict": (False, True),
745
+ "numpy": (False, True),
746
+ "dask.array": (False, True),
747
+ "marray": (False, True),
748
+ },
749
+ int_only=(False, True), test_large_ints=False,
750
+ positive_only=True, torch_native=False,
751
+ ),
752
+ _FuncInfo(
753
+ _ufuncs.nbdtr, 3,
754
+ xp_capabilities(
755
+ cpu_only=True, exceptions=["cupy"],
756
+ jax_jit=True,
757
+ ),
758
+ int_only=(True, True, False), positive_only=True,
759
+ torch_native=False,
760
+ ),
761
+ _FuncInfo(
762
+ _ufuncs.nbdtrc, 3,
763
+ xp_capabilities(
764
+ cpu_only=True, exceptions=["cupy"],
765
+ jax_jit=True,
766
+ ),
767
+ int_only=(True, True, False), positive_only=True,
768
+ torch_native=False,
769
+ ),
770
+ _FuncInfo(
771
+ _ufuncs.nbdtri, 3,
772
+ xp_capabilities(
773
+ cpu_only=True, exceptions=["cupy"],
774
+ jax_jit=True,
775
+ ),
776
+ int_only=(True, True, False), positive_only=True,
777
+ torch_native=False,
778
+ ),
779
+ _FuncInfo(_ufuncs.ndtr, 1),
780
+ _FuncInfo(_ufuncs.ndtri, 1),
781
+ _FuncInfo(
782
+ _ufuncs.pdtr, 2,
783
+ xp_capabilities(
784
+ cpu_only=True, exceptions=["cupy"],
785
+ jax_jit=True,
786
+ ),
787
+ positive_only=True, torch_native=False,
788
+ ),
789
+ _FuncInfo(
790
+ _ufuncs.pdtrc, 2,
791
+ xp_capabilities(
792
+ cpu_only=True, exceptions=["cupy"],
793
+ jax_jit=True,
794
+ ),
795
+ positive_only=True, torch_native=False,
796
+ ),
797
+ _FuncInfo(
798
+ _ufuncs.pdtri, 2,
799
+ xp_capabilities(
800
+ cpu_only=True, exceptions=["cupy"],
801
+ jax_jit=True,
802
+ ),
803
+ int_only=(True, False), positive_only=True,
804
+ torch_native=False,
805
+ ),
806
+ _FuncInfo(
807
+ _ufuncs.poch, 2,
808
+ xp_capabilities(cpu_only=True, exceptions=["cupy", "jax.numpy"]),
809
+ test_large_ints=False, torch_native=False,
810
+ ),
811
+ _FuncInfo(
812
+ _ufuncs.pseudo_huber, 2,
813
+ xp_capabilities(
814
+ cpu_only=True, exceptions=["cupy"],
815
+ jax_jit=True,
816
+ ),
817
+ torch_native=False,
818
+ ),
819
+ _FuncInfo(
820
+ _basic.polygamma, 2, int_only=(True, False), is_ufunc=False,
821
+ scalar_or_0d_only={"torch": (True, False)}, produces_0d=True,
822
+ positive_only={"torch": (True, False), "jax.numpy": True},
823
+ test_large_ints=False,
824
+ ),
825
+ _FuncInfo(_ufuncs.psi, 1, alt_names_map={"jax.numpy": "digamma"}),
826
+ _FuncInfo(
827
+ _ufuncs.radian, 3,
828
+ xp_capabilities(
829
+ cpu_only=True, exceptions=["cupy"],
830
+ jax_jit=True,
831
+ ),
832
+ torch_native=False,
833
+ ),
834
+ _FuncInfo(_ufuncs.rel_entr, 2, generic_impl=_rel_entr),
835
+ _FuncInfo(
836
+ _ufuncs.rgamma, 1,
837
+ xp_capabilities(
838
+ cpu_only=True, exceptions=["cupy"],
839
+ jax_jit=True,
840
+ ),
841
+ torch_native=False,
842
+ ),
843
+ _FuncInfo(
844
+ _basic.sinc, 1,
845
+ xp_capabilities(
846
+ cpu_only=True, exceptions=["cupy", "torch"],
847
+ jax_jit=True,
848
+ ),
849
+ is_ufunc=False,
850
+ backends_with_func_in_xp=("jax.numpy",),
851
+ ),
852
+ _FuncInfo(
853
+ _ufuncs.sindg, 1,
854
+ xp_capabilities(
855
+ cpu_only=True, exceptions=["cupy"],
856
+ jax_jit=True,
857
+ ),
858
+ test_large_ints=False, torch_native=False,
859
+ ),
860
+ _FuncInfo(
861
+ _ufuncs.spence, 1,
862
+ xp_capabilities(cpu_only=True, exceptions=["jax.numpy"]),
863
+ torch_native=False,
864
+ ),
865
+ _FuncInfo(_ufuncs.stdtr, 2, _needs_betainc, generic_impl=_stdtr,
866
+ torch_native=False),
867
+ _FuncInfo(
868
+ _ufuncs.stdtrit, 2,
869
+ xp_capabilities(
870
+ cpu_only=True, exceptions=["cupy"], # needs betainc
871
+ skip_backends=[("jax.numpy", "no scipy.optimize support")],
872
+ ),
873
+ generic_impl=_stdtrit, torch_native=False,
874
+ ),
875
+ _FuncInfo(
876
+ _ufuncs.tandg, 1,
877
+ xp_capabilities(
878
+ cpu_only=True, exceptions=["cupy"],
879
+ jax_jit=True,
880
+ ),
881
+ test_large_ints=False, torch_native=False,
882
+ ),
883
+ _FuncInfo(_ufuncs.xlog1py, 2),
884
+ _FuncInfo(_ufuncs.xlogy, 2, generic_impl=_xlogy),
885
+ _FuncInfo(
886
+ _ufuncs.y0, 1,
887
+ xp_capabilities(
888
+ cpu_only=True, exceptions=["cupy", "torch"],
889
+ jax_jit=True,
890
+ ),
891
+ alt_names_map={"torch": "bessel_y0"}, test_large_ints=False,
892
+ ),
893
+ _FuncInfo(
894
+ _ufuncs.y1, 1,
895
+ xp_capabilities(
896
+ cpu_only=True, exceptions=["cupy", "torch"],
897
+ jax_jit=True,
898
+ ),
899
+ alt_names_map={"torch": "bessel_y1"}, test_large_ints=False,
900
+ ),
901
+ _FuncInfo(
902
+ _ufuncs.yn, 2,
903
+ xp_capabilities(
904
+ cpu_only=True, exceptions=["cupy"],
905
+ jax_jit=True,
906
+ ),
907
+ positive_only={"cupy": (True, False)}, int_only=(True, False),
908
+ test_large_ints=False, torch_native=False,
909
+ ),
910
+ _FuncInfo(
911
+ _basic.zeta, 2, is_ufunc=False,
912
+ positive_only={"jax.numpy": True, "torch": (True, False)},
913
+ test_large_ints=False,
914
+ ),
915
+ _FuncInfo(
916
+ _ufuncs.zetac, 1,
917
+ xp_capabilities(
918
+ cpu_only=True, exceptions=["cupy"],
919
+ jax_jit=True,
920
+ ),
921
+ torch_native=False,
922
+ ),
923
+ )
924
+
925
+ # Override ufuncs.
926
+ # When SCIPY_ARRAY_API is disabled, this exclusively updates the docstrings in place
927
+ # and populates the xp_capabilities table, while retaining the original ufuncs.
928
+ globals().update({nfo.func.__name__: nfo.wrapper for nfo in _special_funcs})
929
+ # digamma is an alias for psi. Define here so it also has alternative backend
930
+ # support. Add noqa because the linter gets confused by the sneaky way psi
931
+ # is inserted into globals above.
932
+ digamma = psi # type:ignore[name-defined] # noqa: F821
933
+ __all__ = [nfo.func.__name__ for nfo in _special_funcs] + ["digamma"]
v12x/pylib/scipy/special/_testutils.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import functools
3
+ import operator
4
+ from scipy._external.packaging_version import version
5
+
6
+ import numpy as np
7
+ from numpy.testing import assert_
8
+ import pytest
9
+
10
+ import scipy.special as sc
11
+
12
+ __all__ = ['with_special_errors', 'assert_func_equal', 'FuncData']
13
+
14
+
15
+ #------------------------------------------------------------------------------
16
+ # Check if a module is present to be used in tests
17
+ #------------------------------------------------------------------------------
18
+
19
+ class MissingModule:
20
+ def __init__(self, name):
21
+ self.name = name
22
+
23
+
24
+ def check_version(module, min_ver):
25
+ if type(module) is MissingModule:
26
+ return pytest.mark.skip(reason=f"{module.name} is not installed")
27
+ return pytest.mark.skipif(
28
+ version.parse(module.__version__) < version.Version(min_ver),
29
+ reason=f"{module.__name__} version >= {min_ver} required"
30
+ )
31
+
32
+
33
+ #------------------------------------------------------------------------------
34
+ # Enable convergence and loss of precision warnings -- turn off one by one
35
+ #------------------------------------------------------------------------------
36
+
37
+ def with_special_errors(func):
38
+ """
39
+ Enable special function errors (such as underflow, overflow,
40
+ loss of precision, etc.)
41
+ """
42
+ @functools.wraps(func)
43
+ def wrapper(*a, **kw):
44
+ with sc.errstate(all='raise'):
45
+ res = func(*a, **kw)
46
+ return res
47
+ return wrapper
48
+
49
+
50
+ #------------------------------------------------------------------------------
51
+ # Comparing function values at many data points at once, with helpful
52
+ # error reports
53
+ #------------------------------------------------------------------------------
54
+
55
+ def assert_func_equal(func, results, points, rtol=None, atol=None,
56
+ param_filter=None, knownfailure=None,
57
+ vectorized=True, dtype=None, nan_ok=False,
58
+ ignore_inf_sign=False, distinguish_nan_and_inf=True):
59
+ if hasattr(points, 'next'):
60
+ # it's a generator
61
+ points = list(points)
62
+
63
+ points = np.asarray(points)
64
+ if points.ndim == 1:
65
+ points = points[:,None]
66
+ nparams = points.shape[1]
67
+
68
+ if hasattr(results, '__name__'):
69
+ # function
70
+ data = points
71
+ result_columns = None
72
+ result_func = results
73
+ else:
74
+ # dataset
75
+ data = np.c_[points, results]
76
+ result_columns = list(range(nparams, data.shape[1]))
77
+ result_func = None
78
+
79
+ fdata = FuncData(func, data, list(range(nparams)),
80
+ result_columns=result_columns, result_func=result_func,
81
+ rtol=rtol, atol=atol, param_filter=param_filter,
82
+ knownfailure=knownfailure, nan_ok=nan_ok, vectorized=vectorized,
83
+ ignore_inf_sign=ignore_inf_sign,
84
+ distinguish_nan_and_inf=distinguish_nan_and_inf)
85
+ fdata.check()
86
+
87
+
88
+ class FuncData:
89
+ """
90
+ Data set for checking a special function.
91
+
92
+ Parameters
93
+ ----------
94
+ func : function
95
+ Function to test
96
+ data : numpy array
97
+ columnar data to use for testing
98
+ param_columns : int or tuple of ints
99
+ Columns indices in which the parameters to `func` lie.
100
+ Can be imaginary integers to indicate that the parameter
101
+ should be cast to complex.
102
+ result_columns : int or tuple of ints, optional
103
+ Column indices for expected results from `func`.
104
+ result_func : callable, optional
105
+ Function to call to obtain results.
106
+ rtol : float, optional
107
+ Required relative tolerance. Default is 5*eps.
108
+ atol : float, optional
109
+ Required absolute tolerance. Default is 5*tiny.
110
+ param_filter : function, or tuple of functions/Nones, optional
111
+ Filter functions to exclude some parameter ranges.
112
+ If omitted, no filtering is done.
113
+ knownfailure : str, optional
114
+ Known failure error message to raise when the test is run.
115
+ If omitted, no exception is raised.
116
+ nan_ok : bool, optional
117
+ If nan is always an accepted result.
118
+ vectorized : bool, optional
119
+ Whether all functions passed in are vectorized.
120
+ ignore_inf_sign : bool, optional
121
+ Whether to ignore signs of infinities.
122
+ (Doesn't matter for complex-valued functions.)
123
+ distinguish_nan_and_inf : bool, optional
124
+ If True, treat numbers which contain nans or infs as
125
+ equal. Sets ignore_inf_sign to be True.
126
+
127
+ """
128
+
129
+ def __init__(self, func, data, param_columns, result_columns=None,
130
+ result_func=None, rtol=None, atol=None, param_filter=None,
131
+ knownfailure=None, dataname=None, nan_ok=False, vectorized=True,
132
+ ignore_inf_sign=False, distinguish_nan_and_inf=True):
133
+ self.func = func
134
+ self.data = data
135
+ self.dataname = dataname
136
+ if not hasattr(param_columns, '__len__'):
137
+ param_columns = (param_columns,)
138
+ self.param_columns = tuple(param_columns)
139
+ if result_columns is not None:
140
+ if not hasattr(result_columns, '__len__'):
141
+ result_columns = (result_columns,)
142
+ self.result_columns = tuple(result_columns)
143
+ if result_func is not None:
144
+ message = "Only result_func or result_columns should be provided"
145
+ raise ValueError(message)
146
+ elif result_func is not None:
147
+ self.result_columns = None
148
+ else:
149
+ raise ValueError("Either result_func or result_columns should be provided")
150
+ self.result_func = result_func
151
+ self.rtol = rtol
152
+ self.atol = atol
153
+ if not hasattr(param_filter, '__len__'):
154
+ param_filter = (param_filter,)
155
+ self.param_filter = param_filter
156
+ self.knownfailure = knownfailure
157
+ self.nan_ok = nan_ok
158
+ self.vectorized = vectorized
159
+ self.ignore_inf_sign = ignore_inf_sign
160
+ self.distinguish_nan_and_inf = distinguish_nan_and_inf
161
+ if not self.distinguish_nan_and_inf:
162
+ self.ignore_inf_sign = True
163
+
164
+ def get_tolerances(self, dtype):
165
+ if not np.issubdtype(dtype, np.inexact):
166
+ dtype = np.dtype(float)
167
+ info = np.finfo(dtype)
168
+ rtol, atol = self.rtol, self.atol
169
+ if rtol is None:
170
+ rtol = 5*info.eps
171
+ if atol is None:
172
+ atol = 5*info.tiny
173
+ return rtol, atol
174
+
175
+ def check(self, data=None, dtype=None, dtypes=None):
176
+ """Check the special function against the data."""
177
+ __tracebackhide__ = operator.methodcaller(
178
+ 'errisinstance', AssertionError
179
+ )
180
+
181
+ if self.knownfailure:
182
+ pytest.xfail(reason=self.knownfailure)
183
+
184
+ if data is None:
185
+ data = self.data
186
+
187
+ if dtype is None:
188
+ dtype = data.dtype
189
+ else:
190
+ data = data.astype(dtype)
191
+
192
+ rtol, atol = self.get_tolerances(dtype)
193
+
194
+ # Apply given filter functions
195
+ if self.param_filter:
196
+ param_mask = np.ones((data.shape[0],), np.bool_)
197
+ for j, filter in zip(self.param_columns, self.param_filter):
198
+ if filter:
199
+ param_mask &= list(filter(data[:,j]))
200
+ data = data[param_mask]
201
+
202
+ # Pick parameters from the correct columns
203
+ params = []
204
+ for idx, j in enumerate(self.param_columns):
205
+ if np.iscomplexobj(j):
206
+ j = int(j.imag)
207
+ params.append(data[:,j].astype(complex))
208
+ elif dtypes and idx < len(dtypes):
209
+ params.append(data[:, j].astype(dtypes[idx]))
210
+ else:
211
+ params.append(data[:,j])
212
+
213
+ # Helper for evaluating results
214
+ def eval_func_at_params(func, skip_mask=None):
215
+ if self.vectorized:
216
+ got = func(*params)
217
+ else:
218
+ got = []
219
+ for j in range(len(params[0])):
220
+ if skip_mask is not None and skip_mask[j]:
221
+ got.append(np.nan)
222
+ continue
223
+ got.append(func(*tuple([params[i][j] for i in range(len(params))])))
224
+ got = np.asarray(got)
225
+ if not isinstance(got, tuple):
226
+ got = (got,)
227
+ return got
228
+
229
+ # Evaluate function to be tested
230
+ got = eval_func_at_params(self.func)
231
+
232
+ # Grab the correct results
233
+ if self.result_columns is not None:
234
+ # Correct results passed in with the data
235
+ wanted = tuple([data[:,icol] for icol in self.result_columns])
236
+ else:
237
+ # Function producing correct results passed in
238
+ skip_mask = None
239
+ if self.nan_ok and len(got) == 1:
240
+ # Don't spend time evaluating what doesn't need to be evaluated
241
+ skip_mask = np.isnan(got[0])
242
+ wanted = eval_func_at_params(self.result_func, skip_mask=skip_mask)
243
+
244
+ # Check the validity of each output returned
245
+ assert_(len(got) == len(wanted))
246
+
247
+ for output_num, (x, y) in enumerate(zip(got, wanted)):
248
+ if np.issubdtype(x.dtype, np.complexfloating) or self.ignore_inf_sign:
249
+ pinf_x = np.isinf(x)
250
+ pinf_y = np.isinf(y)
251
+ minf_x = np.isinf(x)
252
+ minf_y = np.isinf(y)
253
+ else:
254
+ pinf_x = np.isposinf(x)
255
+ pinf_y = np.isposinf(y)
256
+ minf_x = np.isneginf(x)
257
+ minf_y = np.isneginf(y)
258
+ nan_x = np.isnan(x)
259
+ nan_y = np.isnan(y)
260
+
261
+ with np.errstate(all='ignore'):
262
+ abs_y = np.absolute(y)
263
+ abs_y[~np.isfinite(abs_y)] = 0
264
+ diff = np.absolute(x - y)
265
+ diff[~np.isfinite(diff)] = 0
266
+
267
+ rdiff = diff / np.absolute(y)
268
+ rdiff[~np.isfinite(rdiff)] = 0
269
+
270
+ tol_mask = (diff <= atol + rtol*abs_y)
271
+ pinf_mask = (pinf_x == pinf_y)
272
+ minf_mask = (minf_x == minf_y)
273
+
274
+ nan_mask = (nan_x == nan_y)
275
+
276
+ bad_j = ~(tol_mask & pinf_mask & minf_mask & nan_mask)
277
+
278
+ point_count = bad_j.size
279
+ if self.nan_ok:
280
+ bad_j &= ~nan_x
281
+ bad_j &= ~nan_y
282
+ point_count -= (nan_x | nan_y).sum()
283
+
284
+ if not self.distinguish_nan_and_inf and not self.nan_ok:
285
+ # If nan's are okay we've already covered all these cases
286
+ inf_x = np.isinf(x)
287
+ inf_y = np.isinf(y)
288
+ both_nonfinite = (inf_x & nan_y) | (nan_x & inf_y)
289
+ bad_j &= ~both_nonfinite
290
+ point_count -= both_nonfinite.sum()
291
+
292
+ if np.any(bad_j):
293
+ # Some bad results: inform what, where, and how bad
294
+ msg = [""]
295
+ msg.append(f"Max |adiff|: {diff[bad_j].max():g}")
296
+ msg.append(f"Max |rdiff|: {rdiff[bad_j].max():g}")
297
+ msg.append(f"Bad results ({np.sum(bad_j)} out of "
298
+ f"{point_count}) for the following points "
299
+ f"(in output {output_num}):")
300
+ for j in np.nonzero(bad_j)[0]:
301
+ j = int(j)
302
+ def fmt(x):
303
+ return f'{np.array2string(x[j], precision=18):30s}'
304
+ a = " ".join(map(fmt, params))
305
+ b = " ".join(map(fmt, got))
306
+ c = " ".join(map(fmt, wanted))
307
+ d = fmt(rdiff)
308
+ msg.append(f"{a} => {b} != {c} (rdiff {d})")
309
+ assert_(False, "\n".join(msg))
310
+
311
+ def __repr__(self):
312
+ """Pretty-printing"""
313
+ if np.any(list(map(np.iscomplexobj, self.param_columns))):
314
+ is_complex = " (complex)"
315
+ else:
316
+ is_complex = ""
317
+ if self.dataname:
318
+ return (f"<Data for {self.func.__name__}{is_complex}: "
319
+ f"{os.path.basename(self.dataname)}>")
320
+ else:
321
+ return f"<Data for {self.func.__name__}{is_complex}>"
v12x/pylib/scipy/special/_ufunc_tools.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helpers for producing efficient wrappers of ufuncs.
2
+ """
3
+
4
+ import re
5
+ import numpy as np
6
+
7
+
8
+ def _parse_core_ndims(signature):
9
+ """Return tuple of num core dims per input from gufunc signature."""
10
+ input_sig = signature.split('->')[0]
11
+ groups = re.findall(r"\((.*?)\)", input_sig)
12
+ return tuple(0 if not g.strip() else g.count(',') + 1 for g in groups)
13
+
14
+
15
+ def _with_cache_optimization(
16
+ *,
17
+ name,
18
+ arg_names,
19
+ docstring,
20
+ ufunc,
21
+ cache_arg_indices,
22
+ ):
23
+ """Helper to ensure optimal iteration order for ufuncs that use caching.
24
+
25
+ This concerns internal caches which are only live over the course of one
26
+ call to a ufunc to avoid repeated computations during the course of the
27
+ loops. See the notes below for more information.
28
+
29
+ Parameters
30
+ ----------
31
+ name : str
32
+ arg_names : list[str]
33
+ The function name and arg names are passed in so that the wrapper
34
+ can be generated with the create name and argument names, improving
35
+ documentation and autocomplete.
36
+ docstring : str
37
+ ufunc : numpy.ufunc
38
+ cache_arg_indices : list[int]
39
+ Arguments to ufunc which are used in the kernel to compute an output
40
+ which is being cached for reuse when iterating over other arguments.
41
+
42
+ Returns
43
+ -------
44
+ callable
45
+ A wrapper for ufunc which transposes the axes of the inputs to ensure
46
+ iteration precedes in such a way to allow the cache within the ufunc
47
+ kernel to eliminate redundant computation.
48
+
49
+ Notes
50
+ -----
51
+ There is a common pattern in ufunc kernels exemplified by the situation
52
+ where some of the arguments are used to compute coeffients of an expansion
53
+ that is taken over one or more of the other arguments. A classic example is
54
+ Mathieu functions, which compute coefficients corresponding to the shape
55
+ parameter q and order m which in principle could be reused for varying
56
+ values of the parameter x.
57
+
58
+ It had long been the case that the expensive computation of coefficients is
59
+ repeated unnecessarily across values of x. It is possible to add a cache to
60
+ the ufunc kernel which stores the expansion coefficients and only updates
61
+ if the pointers into the q and m arrays advance during the course of the
62
+ ufunc loops. Such a cache is instantiated each time a ufunc is called and
63
+ only lives during the course of the loops that are carried out for that
64
+ particular call.
65
+
66
+ Whether such a cache actually helps depends on the order in which iteration
67
+ occurs. Ideally, one would want q and m to advance most slowly and for the
68
+ iterations over x for fixed q and m to be pushed to the inner most loops.
69
+ This helper replaces each input array (and a pre-allocated output array)
70
+ with a view where the axes which should vary most slowly are transposed to
71
+ the ends and forces computation in C order. This ensures iteration proceeds
72
+ in the optimal order.
73
+
74
+ Note that because the pre-allocated output array used internally is C
75
+ contiguous, the output will be C contiguous regardless of contiguity of
76
+ the inputs.
77
+
78
+ """
79
+
80
+ # Need to keep track of the number of core dimensions for each input
81
+ # since core dimensions don't participate in broadcasting.
82
+ core_ndims = (
83
+ _parse_core_ndims(ufunc.signature)
84
+ if ufunc.signature is not None
85
+ else (0,)*ufunc.nin
86
+ )
87
+
88
+ def _wrapper(*args):
89
+ args = [np.asarray(arg) for arg in args]
90
+
91
+ # Fast path for when the arguments which are used in the cached
92
+ # computation don't have batches.
93
+ if all(args[i].ndim == core_ndims[i] for i in cache_arg_indices):
94
+ return ufunc(*args)
95
+
96
+ # To get batch_shapes, need to exclude core dimensions. Again, the core
97
+ # dimensions won't participate in broadcasting.
98
+ batch_shapes = [
99
+ arg.shape[:-core_ndims[i]] if core_ndims[i] > 0 else arg.shape
100
+ for i, arg in enumerate(args)
101
+ ]
102
+ batch_shape = np.broadcast_shapes(*batch_shapes)
103
+ batch_ndim = len(batch_shape)
104
+
105
+ # Broadcast each arg so that the batch shapes all agree, but the
106
+ # core dimensions may still differ.
107
+ args_b = [
108
+ np.broadcast_to(arg, batch_shape + arg.shape[-core_ndims[i]:])
109
+ if core_ndims[i] > 0 else np.broadcast_to(arg, batch_shape)
110
+ for i, arg in enumerate(args)
111
+ ]
112
+
113
+ # After broadcasting, determine which axes have stride-length
114
+ # zero for each of the args participating in the cache. The cached
115
+ # value will remain the same for iterations across these axes.
116
+ varying_axes = []
117
+ constant_axes = []
118
+
119
+ for ax in range(batch_ndim):
120
+ is_constant = all(
121
+ args_b[i].strides[ax] == 0 for i in cache_arg_indices
122
+ )
123
+ if is_constant:
124
+ constant_axes.append(ax)
125
+ else:
126
+ varying_axes.append(ax)
127
+ sorted_batch_axes = varying_axes + constant_axes
128
+
129
+ # Push the non-varying axes to the end so that they will vary most
130
+ # slowly when iterating in C order.
131
+ args_t = []
132
+ for i, arg_b in enumerate(args_b):
133
+ axes = sorted_batch_axes + list(
134
+ range(batch_ndim, batch_ndim + core_ndims[i])
135
+ )
136
+ args_t.append(np.transpose(arg_b, axes=axes))
137
+
138
+ # Premake the output array.
139
+ input_dtypes = tuple(arg.dtype for arg in args_t) + (None,)*ufunc.nout
140
+ out_dtype = ufunc.resolve_dtypes(input_dtypes)[-1]
141
+ out_final = np.empty(batch_shape, dtype=out_dtype, order='C')
142
+ # a view of the output array with axes sorted as needed.
143
+ out_t = np.transpose(out_final, axes=sorted_batch_axes)
144
+
145
+ # Set out to the above view, but return the C contiguous output.
146
+ # This avoids having non-contiguous output.
147
+ ufunc(*args_t, out=out_t, order='C')
148
+ return out_final
149
+
150
+ # Do some metaprogramming with exec so that the arg names and func
151
+ # name are as expected.
152
+ arg_str = ", ".join(arg_names)
153
+ code = (
154
+ f"""def {name}({arg_str}):
155
+ return _wrapper({arg_str})
156
+ """
157
+ )
158
+ namespace = {"_wrapper": _wrapper}
159
+ exec(code, namespace)
160
+ wrapper = namespace[name]
161
+ wrapper.__doc__ = docstring
162
+ return wrapper
v12x/pylib/scipy/special/add_newdocs.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is not meant for public use and will be removed in SciPy v2.0.0.
2
+
3
+ from scipy._lib.deprecation import _sub_module_deprecation
4
+
5
+ __all__: list[str] = []
6
+
7
+
8
+ def __dir__():
9
+ return __all__
10
+
11
+
12
+ def __getattr__(name):
13
+ return _sub_module_deprecation(sub_package="special", module="add_newdocs",
14
+ private_modules=["_add_newdocs"], all=__all__,
15
+ attribute=name)
v12x/pylib/scipy/special/basic.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is not meant for public use and will be removed in SciPy v2.0.0.
2
+ # Use the `scipy.special` namespace for importing the functions
3
+ # included below.
4
+
5
+ from scipy._lib.deprecation import _sub_module_deprecation
6
+
7
+
8
+ __all__ = [ # noqa: F822
9
+ 'ai_zeros',
10
+ 'assoc_laguerre',
11
+ 'bei_zeros',
12
+ 'beip_zeros',
13
+ 'ber_zeros',
14
+ 'bernoulli',
15
+ 'berp_zeros',
16
+ 'bi_zeros',
17
+ 'comb',
18
+ 'digamma',
19
+ 'diric',
20
+ 'erf_zeros',
21
+ 'euler',
22
+ 'factorial',
23
+ 'factorial2',
24
+ 'factorialk',
25
+ 'fresnel_zeros',
26
+ 'fresnelc_zeros',
27
+ 'fresnels_zeros',
28
+ 'gamma',
29
+ 'h1vp',
30
+ 'h2vp',
31
+ 'hankel1',
32
+ 'hankel2',
33
+ 'iv',
34
+ 'ivp',
35
+ 'jn_zeros',
36
+ 'jnjnp_zeros',
37
+ 'jnp_zeros',
38
+ 'jnyn_zeros',
39
+ 'jv',
40
+ 'jvp',
41
+ 'kei_zeros',
42
+ 'keip_zeros',
43
+ 'kelvin_zeros',
44
+ 'ker_zeros',
45
+ 'kerp_zeros',
46
+ 'kv',
47
+ 'kvp',
48
+ 'lmbda',
49
+ 'lqmn',
50
+ 'lqn',
51
+ 'mathieu_a',
52
+ 'mathieu_b',
53
+ 'mathieu_even_coef',
54
+ 'mathieu_odd_coef',
55
+ 'obl_cv_seq',
56
+ 'pbdn_seq',
57
+ 'pbdv_seq',
58
+ 'pbvv_seq',
59
+ 'perm',
60
+ 'polygamma',
61
+ 'pro_cv_seq',
62
+ 'psi',
63
+ 'riccati_jn',
64
+ 'riccati_yn',
65
+ 'sinc',
66
+ 'y0_zeros',
67
+ 'y1_zeros',
68
+ 'y1p_zeros',
69
+ 'yn_zeros',
70
+ 'ynp_zeros',
71
+ 'yv',
72
+ 'yvp',
73
+ 'zeta'
74
+ ]
75
+
76
+
77
+ def __dir__():
78
+ return __all__
79
+
80
+
81
+ def __getattr__(name):
82
+ return _sub_module_deprecation(sub_package="special", module="basic",
83
+ private_modules=["_basic", "_ufuncs"], all=__all__,
84
+ attribute=name)
v12x/pylib/scipy/special/orthogonal.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is not meant for public use and will be removed in SciPy v2.0.0.
2
+ # Use the `scipy.special` namespace for importing the functions
3
+ # included below.
4
+
5
+ from scipy._lib.deprecation import _sub_module_deprecation
6
+
7
+
8
+ _polyfuns = ['legendre', 'chebyt', 'chebyu', 'chebyc', 'chebys',
9
+ 'jacobi', 'laguerre', 'genlaguerre', 'hermite',
10
+ 'hermitenorm', 'gegenbauer', 'sh_legendre', 'sh_chebyt',
11
+ 'sh_chebyu', 'sh_jacobi']
12
+
13
+ # Correspondence between new and old names of root functions
14
+ _rootfuns_map = {'roots_legendre': 'p_roots',
15
+ 'roots_chebyt': 't_roots',
16
+ 'roots_chebyu': 'u_roots',
17
+ 'roots_chebyc': 'c_roots',
18
+ 'roots_chebys': 's_roots',
19
+ 'roots_jacobi': 'j_roots',
20
+ 'roots_laguerre': 'l_roots',
21
+ 'roots_genlaguerre': 'la_roots',
22
+ 'roots_hermite': 'h_roots',
23
+ 'roots_hermitenorm': 'he_roots',
24
+ 'roots_gegenbauer': 'cg_roots',
25
+ 'roots_sh_legendre': 'ps_roots',
26
+ 'roots_sh_chebyt': 'ts_roots',
27
+ 'roots_sh_chebyu': 'us_roots',
28
+ 'roots_sh_jacobi': 'js_roots'}
29
+
30
+
31
+ __all__ = _polyfuns + list(_rootfuns_map.keys()) + [ # noqa: F822
32
+ 'airy', 'p_roots', 't_roots', 'u_roots', 'c_roots', 's_roots',
33
+ 'j_roots', 'l_roots', 'la_roots', 'h_roots', 'he_roots', 'cg_roots',
34
+ 'ps_roots', 'ts_roots', 'us_roots', 'js_roots'
35
+ ]
36
+
37
+
38
+ def __dir__():
39
+ return __all__
40
+
41
+
42
+ def __getattr__(name):
43
+ return _sub_module_deprecation(sub_package="special", module="orthogonal",
44
+ private_modules=["_orthogonal"], all=__all__,
45
+ attribute=name)
v12x/pylib/scipy/special/sf_error.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is not meant for public use and will be removed in SciPy v2.0.0.
2
+ # Use the `scipy.special` namespace for importing the functions
3
+ # included below.
4
+
5
+ from scipy._lib.deprecation import _sub_module_deprecation
6
+
7
+ __all__ = [ # noqa: F822
8
+ 'SpecialFunctionWarning',
9
+ 'SpecialFunctionError'
10
+ ]
11
+
12
+
13
+ def __dir__():
14
+ return __all__
15
+
16
+
17
+ def __getattr__(name):
18
+ return _sub_module_deprecation(sub_package="special", module="sf_error",
19
+ private_modules=["_sf_error"], all=__all__,
20
+ attribute=name)
v12x/pylib/scipy/special/specfun.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is not meant for public use and will be removed in SciPy v2.0.0.
2
+ # Use the `scipy.special` namespace for importing the functions
3
+ # included below.
4
+
5
+ from scipy._lib.deprecation import _sub_module_deprecation
6
+
7
+ # ruff: noqa: F822
8
+ __all__ = [
9
+ 'lqmn',
10
+ 'pbdv'
11
+ ]
12
+
13
+
14
+ def __dir__():
15
+ return __all__
16
+
17
+
18
+ def __getattr__(name):
19
+ return _sub_module_deprecation(sub_package="special", module="specfun",
20
+ private_modules=["_basic", "_specfun"], all=__all__,
21
+ attribute=name)
v12x/pylib/scipy/special/spfun_stats.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is not meant for public use and will be removed in SciPy v2.0.0.
2
+ # Use the `scipy.special` namespace for importing the functions
3
+ # included below.
4
+
5
+ from scipy._lib.deprecation import _sub_module_deprecation
6
+
7
+ __all__ = ['multigammaln'] # noqa: F822
8
+
9
+
10
+ def __dir__():
11
+ return __all__
12
+
13
+
14
+ def __getattr__(name):
15
+ return _sub_module_deprecation(sub_package="special", module="spfun_stats",
16
+ private_modules=["_spfun_stats"], all=__all__,
17
+ attribute=name)