|
14 | 14 | import math
|
15 | 15 | import numpy as np
|
16 | 16 | import spatialmath.base as smb
|
| 17 | +from spatialmath.base.argcheck import getunit |
17 | 18 | from spatialmath.base.types import *
|
| 19 | +import scipy.interpolate as interpolate |
| 20 | +from typing import Optional |
| 21 | +from functools import lru_cache |
18 | 22 |
|
19 | 23 | _eps = np.finfo(np.float64).eps
|
20 | 24 |
|
21 |
| - |
22 | 25 | def qeye() -> QuaternionArray:
|
23 | 26 | """
|
24 | 27 | Create an identity quaternion
|
@@ -843,29 +846,112 @@ def qslerp(
|
843 | 846 | return q0
|
844 | 847 |
|
845 | 848 |
|
846 |
| -def qrand() -> UnitQuaternionArray: |
| 849 | +def _compute_cdf_sin_squared(theta: float): |
847 | 850 | """
|
848 |
| - Random unit-quaternion |
| 851 | + Computes the CDF for the distribution of angular magnitude for uniformly sampled rotations. |
| 852 | + |
| 853 | + :arg theta: angular magnitude |
| 854 | + :rtype: float |
| 855 | + :return: cdf of a given angular magnitude |
| 856 | + :rtype: float |
| 857 | +
|
| 858 | + Helper function for uniform sampling of rotations with constrained angular magnitude. |
| 859 | + This function returns the integral of the pdf of angular magnitudes (2/pi * sin^2(theta/2)). |
| 860 | + """ |
| 861 | + return (theta - np.sin(theta)) / np.pi |
849 | 862 |
|
| 863 | +@lru_cache(maxsize=1) |
| 864 | +def _generate_inv_cdf_sin_squared_interp(num_interpolation_points: int = 256) -> interpolate.interp1d: |
| 865 | + """ |
| 866 | + Computes an interpolation function for the inverse CDF of the distribution of angular magnitude. |
| 867 | + |
| 868 | + :arg num_interpolation_points: number of points to use in the interpolation function |
| 869 | + :rtype: int |
| 870 | + :return: interpolation function for the inverse cdf of a given angular magnitude |
| 871 | + :rtype: interpolate.interp1d |
| 872 | +
|
| 873 | + Helper function for uniform sampling of rotations with constrained angular magnitude. |
| 874 | + This function returns interpolation function for the inverse of the integral of the |
| 875 | + pdf of angular magnitudes (2/pi * sin^2(theta/2)), which is not analytically defined. |
| 876 | + """ |
| 877 | + cdf_sin_squared_interp_angles = np.linspace(0, np.pi, num_interpolation_points) |
| 878 | + cdf_sin_squared_interp_values = _compute_cdf_sin_squared(cdf_sin_squared_interp_angles) |
| 879 | + return interpolate.interp1d(cdf_sin_squared_interp_values, cdf_sin_squared_interp_angles) |
| 880 | + |
| 881 | +def _compute_inv_cdf_sin_squared(x: ArrayLike, num_interpolation_points: int = 256) -> ArrayLike: |
| 882 | + """ |
| 883 | + Computes the inverse CDF of the distribution of angular magnitude. |
| 884 | + |
| 885 | + :arg x: value for cdf of angular magnitudes |
| 886 | + :rtype: ArrayLike |
| 887 | + :arg num_interpolation_points: number of points to use in the interpolation function |
| 888 | + :rtype: int |
| 889 | + :return: angular magnitude associate with cdf value |
| 890 | + :rtype: ArrayLike |
| 891 | +
|
| 892 | + Helper function for uniform sampling of rotations with constrained angular magnitude. |
| 893 | + This function returns the angle associated with the cdf value derived form integral of |
| 894 | + the pdf of angular magnitudes (2/pi * sin^2(theta/2)), which is not analytically defined. |
| 895 | + """ |
| 896 | + inv_cdf_sin_squared_interp = _generate_inv_cdf_sin_squared_interp(num_interpolation_points) |
| 897 | + return inv_cdf_sin_squared_interp(x) |
| 898 | + |
| 899 | +def qrand(theta_range:Optional[ArrayLike2] = None, unit: str = "rad", num_interpolation_points: int = 256) -> UnitQuaternionArray: |
| 900 | + """ |
| 901 | + Random unit-quaternion |
| 902 | + |
| 903 | + :arg theta_range: angular magnitude range [min,max], defaults to None. |
| 904 | + :type xrange: 2-element sequence, optional |
| 905 | + :arg unit: angular units: 'rad' [default], or 'deg' |
| 906 | + :type unit: str |
| 907 | + :arg num_interpolation_points: number of points to use in the interpolation function |
| 908 | + :rtype: int |
| 909 | + :arg num_interpolation_points: number of points to use in the interpolation function |
| 910 | + :rtype: int |
850 | 911 | :return: random unit-quaternion
|
851 | 912 | :rtype: ndarray(4)
|
852 | 913 |
|
853 |
| - Computes a uniformly distributed random unit-quaternion which can be |
854 |
| - considered equivalent to a random SO(3) rotation. |
| 914 | + Computes a uniformly distributed random unit-quaternion, with in a maximum |
| 915 | + angular magnitude, which can be considered equivalent to a random SO(3) rotation. |
855 | 916 |
|
856 | 917 | .. runblock:: pycon
|
857 | 918 |
|
858 | 919 | >>> from spatialmath.base import qrand, qprint
|
859 | 920 | >>> qprint(qrand())
|
860 | 921 | """
|
861 |
| - u = np.random.uniform(low=0, high=1, size=3) # get 3 random numbers in [0,1] |
862 |
| - return np.r_[ |
863 |
| - math.sqrt(1 - u[0]) * math.sin(2 * math.pi * u[1]), |
864 |
| - math.sqrt(1 - u[0]) * math.cos(2 * math.pi * u[1]), |
865 |
| - math.sqrt(u[0]) * math.sin(2 * math.pi * u[2]), |
866 |
| - math.sqrt(u[0]) * math.cos(2 * math.pi * u[2]), |
867 |
| - ] |
| 922 | + if theta_range is not None: |
| 923 | + theta_range = getunit(theta_range, unit) |
| 924 | + |
| 925 | + if(theta_range[0] < 0 or theta_range[1] > np.pi or theta_range[0] > theta_range[1]): |
| 926 | + ValueError('Invalid angular range. Must be within the range[0, pi].' |
| 927 | + + f' Recieved {theta_range}.') |
| 928 | + |
| 929 | + # Sample axis and angle independently, respecting the CDF of the |
| 930 | + # angular magnitude under uniform sampling. |
| 931 | + |
| 932 | + # Sample angle using inverse transform sampling based on CDF |
| 933 | + # of the angular distribution (2/pi * sin^2(theta/2)) |
| 934 | + theta = _compute_inv_cdf_sin_squared( |
| 935 | + np.random.uniform( |
| 936 | + low=_compute_cdf_sin_squared(theta_range[0]), |
| 937 | + high=_compute_cdf_sin_squared(theta_range[1]), |
| 938 | + ), |
| 939 | + num_interpolation_points=num_interpolation_points, |
| 940 | + ) |
| 941 | + # Sample axis uniformly using 3D normal distributed |
| 942 | + v = np.random.randn(3) |
| 943 | + v /= np.linalg.norm(v) |
868 | 944 |
|
| 945 | + return np.r_[math.cos(theta / 2), (math.sin(theta / 2) * v)] |
| 946 | + else: |
| 947 | + u = np.random.uniform(low=0, high=1, size=3) # get 3 random numbers in [0,1] |
| 948 | + return np.r_[ |
| 949 | + math.sqrt(1 - u[0]) * math.sin(2 * math.pi * u[1]), |
| 950 | + math.sqrt(1 - u[0]) * math.cos(2 * math.pi * u[1]), |
| 951 | + math.sqrt(u[0]) * math.sin(2 * math.pi * u[2]), |
| 952 | + math.sqrt(u[0]) * math.cos(2 * math.pi * u[2]), |
| 953 | + ] |
| 954 | + |
869 | 955 |
|
870 | 956 | def qmatrix(q: ArrayLike4) -> R4x4:
|
871 | 957 | """
|
|
0 commit comments