sync 2026-10-29 07:40 GMT (part 7)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- v12x/pylib/scipy-1.18.1.dist-info/sboms/auditwheel.cdx.json +1 -0
- v12x/pylib/scipy/spatial/transform/__init__.py +31 -0
- v12x/pylib/scipy/spatial/transform/_rigid_transform.py +1975 -0
- v12x/pylib/scipy/spatial/transform/_rigid_transform_xp.py +448 -0
- v12x/pylib/scipy/spatial/transform/_rotation.py +0 -0
- v12x/pylib/scipy/spatial/transform/_rotation_groups.py +140 -0
- v12x/pylib/scipy/spatial/transform/_rotation_spline.py +465 -0
- v12x/pylib/scipy/spatial/transform/_rotation_xp.py +1140 -0
- v12x/pylib/scipy/spatial/transform/rotation.py +21 -0
- v12x/pylib/scipy/spatial/transform/tests/__init__.py +0 -0
- v12x/pylib/scipy/spatial/transform/tests/test_rigid_transform.py +1531 -0
- v12x/pylib/scipy/spatial/transform/tests/test_rotation.py +0 -0
- v12x/pylib/scipy/spatial/transform/tests/test_rotation_groups.py +169 -0
- v12x/pylib/scipy/spatial/transform/tests/test_rotation_spline.py +188 -0
- v12x/pylib/scipy/special/__init__.py +842 -0
- v12x/pylib/scipy/special/_add_newdocs.py +0 -0
- v12x/pylib/scipy/special/_basic.py +0 -0
- v12x/pylib/scipy/special/_ellip_harm.py +214 -0
- v12x/pylib/scipy/special/_input_validation.py +17 -0
- v12x/pylib/scipy/special/_lambertw.py +149 -0
- v12x/pylib/scipy/special/_logsumexp.py +420 -0
- v12x/pylib/scipy/special/_mptestutils.py +453 -0
- v12x/pylib/scipy/special/_multiufuncs.py +744 -0
- v12x/pylib/scipy/special/_orthogonal.py +2856 -0
- v12x/pylib/scipy/special/_precompute/__init__.py +0 -0
- v12x/pylib/scipy/special/_precompute/cosine_cdf.py +17 -0
- v12x/pylib/scipy/special/_precompute/expn_asy.py +54 -0
- v12x/pylib/scipy/special/_precompute/gammainc_asy.py +116 -0
- v12x/pylib/scipy/special/_precompute/gammainc_data.py +124 -0
- v12x/pylib/scipy/special/_precompute/hyp2f1_data.py +484 -0
- v12x/pylib/scipy/special/_precompute/lambertw.py +68 -0
- v12x/pylib/scipy/special/_precompute/loggamma.py +43 -0
- v12x/pylib/scipy/special/_precompute/struve_convergence.py +131 -0
- v12x/pylib/scipy/special/_precompute/utils.py +38 -0
- v12x/pylib/scipy/special/_precompute/wright_bessel.py +342 -0
- v12x/pylib/scipy/special/_precompute/wright_bessel_data.py +152 -0
- v12x/pylib/scipy/special/_precompute/wrightomega.py +41 -0
- v12x/pylib/scipy/special/_precompute/zetac.py +27 -0
- v12x/pylib/scipy/special/_sf_error.py +15 -0
- v12x/pylib/scipy/special/_spfun_stats.py +178 -0
- v12x/pylib/scipy/special/_spherical_bessel.py +401 -0
- v12x/pylib/scipy/special/_support_alternative_backends.py +933 -0
- v12x/pylib/scipy/special/_testutils.py +321 -0
- v12x/pylib/scipy/special/_ufunc_tools.py +162 -0
- v12x/pylib/scipy/special/add_newdocs.py +15 -0
- v12x/pylib/scipy/special/basic.py +84 -0
- v12x/pylib/scipy/special/orthogonal.py +45 -0
- v12x/pylib/scipy/special/sf_error.py +20 -0
- v12x/pylib/scipy/special/specfun.py +21 -0
- 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)
|