From 3d21b065178cc7543e834c42d0bcf9a3936e1ee9 Mon Sep 17 00:00:00 2001 From: Sebastian Castro <169091242+scastro-bdai@users.noreply.github.com> Date: Mon, 30 Dec 2024 11:49:31 -0500 Subject: [PATCH 01/14] Fix string equality warnings in `SplineFit` class (#147) --- spatialmath/spline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spatialmath/spline.py b/spatialmath/spline.py index 0a472ecc..8d30bd80 100644 --- a/spatialmath/spline.py +++ b/spatialmath/spline.py @@ -192,14 +192,14 @@ def stochastic_downsample_interpolation( ) sample_time = self.time_data[candidate_removal_index] - if check_type is "local": + if check_type == "local": angular_error = SO3(self.pose_data[candidate_removal_index]).angdist( SO3(self.spline.spline_so3(sample_time).as_matrix()) ) euclidean_error = np.linalg.norm( self.pose_data[candidate_removal_index].t - self.spline.spline_xyz(sample_time) ) - elif check_type is "global": + elif check_type == "global": angular_error = self.max_angular_error() euclidean_error = self.max_euclidean_error() else: From 7fe17c9ade4b9620b5e0564094a58472c28d5530 Mon Sep 17 00:00:00 2001 From: Ben Axelrod Date: Wed, 8 Jan 2025 10:14:52 -0500 Subject: [PATCH 02/14] [SW-1712] Pin ubuntu version in workflows (#148) --- .github/workflows/master.yml | 4 ++-- .github/workflows/publish.yml | 2 +- .github/workflows/sphinx.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 19e2f3fb..23deebe9 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-latest, ubuntu-latest, macos-latest] + os: [windows-latest, ubuntu-22.04, macos-latest] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] exclude: - os: windows-latest @@ -44,7 +44,7 @@ jobs: # If all tests pass: # Run coverage and upload to codecov needs: unittest - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.8 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fc49c0ed..e2d439fd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: strategy: max-parallel: 2 matrix: - os: [ubuntu-latest] + os: [ubuntu-22.04] python-version: [3.8] steps: diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 7ce7d443..9b24d2b3 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -5,7 +5,7 @@ on: jobs: sphinx: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: ${{ github.event_name != 'pull_request' }} steps: - uses: actions/checkout@v2 From 0ff5c8318d1e1c34c69f3a13db8ed8639348a492 Mon Sep 17 00:00:00 2001 From: jbarnett-bdai Date: Mon, 13 Jan 2025 10:30:08 -0500 Subject: [PATCH 03/14] Optional deps humble (#150) --- README.md | 7 +++++++ pyproject.toml | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3efadf10..a2db78e4 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,13 @@ Install a snapshot from PyPI pip install spatialmath-python ``` +Note that if you are using ROS2, you may run into version conflicts when using `rosdep`, particularly +concerning `matplotlib`. If this happens, you can enable optional version pinning with + +``` +pip install spatialmath-python[ros-humble] +``` + ## From GitHub Install the current code base from GitHub and pip install a link to that cloned copy diff --git a/pyproject.toml b/pyproject.toml index cead2ac3..4295f2f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,9 +36,9 @@ keywords = [ ] dependencies = [ - "numpy>=1.22,<2", # Cannot use 2.0 due to matplotlib version pinning. + "numpy>=1.22", "scipy", - "matplotlib==3.5.1", # Large user-base has apt-installed python3-matplotlib (ROS2) which is pinned to this version. + "matplotlib", "ansitable", "typing_extensions", "pre-commit", @@ -70,6 +70,11 @@ docs = [ "sphinx-autodoc-typehints", ] +ros-humble = [ + "matplotlib==3.5.1", # Large user-base has apt-installed python3-matplotlib (ROS2) which is pinned to this version. + "numpy<2", # Cannot use 2.0 due to matplotlib version pinning. +] + [build-system] requires = ["setuptools", "oldest-supported-numpy"] From 12128c595223493297ef2a0dbd55232891b04493 Mon Sep 17 00:00:00 2001 From: Mark Yeatman <129521731+myeatman-bdai@users.noreply.github.com> Date: Wed, 22 Jan 2025 08:02:20 -0500 Subject: [PATCH 04/14] Temporarily remove pre commit config. (#153) --- .pre-commit-config.yaml | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 7c28f075..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,35 +0,0 @@ -repos: -- repo: https://github.com/charliermarsh/ruff-pre-commit - # Ruff version. - rev: 'v0.1.0' - hooks: - - id: ruff - args: ['--fix', '--config', 'pyproject.toml'] - -- repo: https://github.com/psf/black - rev: 23.10.0 - hooks: - - id: black - language_version: python3.10 - args: ['--config', 'pyproject.toml'] - verbose: true - -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 - hooks: - - id: end-of-file-fixer - - id: debug-statements # Ensure we don't commit `import pdb; pdb.set_trace()` - exclude: | - (?x)^( - docker/ros/web/static/.*| - )$ - - id: trailing-whitespace - exclude: | - (?x)^( - docker/ros/web/static/.*| - (.*/).*\.patch| - )$ -# - repo: https://github.com/pre-commit/mirrors-mypy -# rev: v1.6.1 -# hooks: -# - id: mypy From 23066289d4e32b1b03ac99529fc44bd8a2427c2b Mon Sep 17 00:00:00 2001 From: Jien Cao <135634522+jcao-bdai@users.noreply.github.com> Date: Wed, 22 Jan 2025 08:12:11 -0500 Subject: [PATCH 05/14] Bug fixes [Issue-136], [Issue-137] (#138) Co-authored-by: Mark Yeatman <129521731+myeatman-bdai@users.noreply.github.com> --- .github/workflows/sphinx.yml | 4 ++-- spatialmath/base/graphics.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 9b24d2b3..45105ee5 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -9,10 +9,10 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - uses: actions/checkout@v2 - - name: Set up Python 3.7 + - name: Set up Python 3.8 uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/spatialmath/base/graphics.py b/spatialmath/base/graphics.py index 2ce18dc8..c51d7f94 100644 --- a/spatialmath/base/graphics.py +++ b/spatialmath/base/graphics.py @@ -982,7 +982,7 @@ def plot_sphere( handles = [] for c in centre.T: X, Y, Z = sphere(centre=c, radius=radius, resolution=resolution) - handles.append(_render3D(ax, X, Y, Z, **kwargs)) + handles.append(_render3D(ax, X, Y, Z, pose=pose, **kwargs)) return handles @@ -1214,7 +1214,7 @@ def plot_cylinder( ) # Pythagorean theorem handles = [] - handles.append(_render3D(ax, X, Y, Z, filled=filled, **kwargs)) + handles.append(_render3D(ax, X, Y, Z, filled=filled, pose=pose, **kwargs)) handles.append( _render3D(ax, X, (2 * centre[1] - Y), Z, filled=filled, pose=pose, **kwargs) ) @@ -1298,9 +1298,9 @@ def plot_cone( Z = height - Z handles = [] - handles.append(_render3D(ax, X, Y, Z, filled=filled, **kwargs)) + handles.append(_render3D(ax, X, Y, Z, pose=pose, filled=filled, **kwargs)) handles.append( - _render3D(ax, X, (2 * centre[1] - Y), Z, filled=filled, **kwargs) + _render3D(ax, X, (2 * centre[1] - Y), Z, pose=pose, filled=filled, **kwargs) ) if ends and kwargs.get("filled", default=False): From 59873f14c92e0c490de7bef523e10f5438d38263 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Sun, 26 Jan 2025 17:00:08 +1100 Subject: [PATCH 06/14] compute mean of a set of rotations (#160) --- spatialmath/pose3d.py | 43 ++++++++++++---- spatialmath/quaternion.py | 78 +++++++++++++++++++++++------ tests/test_pose3d.py | 31 ++++++++++++ tests/test_quaternion.py | 100 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 223 insertions(+), 29 deletions(-) diff --git a/spatialmath/pose3d.py b/spatialmath/pose3d.py index b4301d93..4e558512 100644 --- a/spatialmath/pose3d.py +++ b/spatialmath/pose3d.py @@ -843,22 +843,22 @@ def Exp( def UnitQuaternion(self) -> UnitQuaternion: """ - SO3 as a unit quaternion instance + SO3 as a unit quaternion instance - :return: a unit quaternion representation - :rtype: UnitQuaternion instance + :return: a unit quaternion representation + :rtype: UnitQuaternion instance - ``R.UnitQuaternion()`` is an ``UnitQuaternion`` instance representing the same rotation - as the SO3 rotation ``R``. + ``R.UnitQuaternion()`` is an ``UnitQuaternion`` instance representing the same rotation + as the SO3 rotation ``R``. - Example: + Example: - .. runblock:: pycon + .. runblock:: pycon - >>> from spatialmath import SO3 - >>> SO3.Rz(0.3).UnitQuaternion() + >>> from spatialmath import SO3 + >>> SO3.Rz(0.3).UnitQuaternion() - """ + """ # Function level import to avoid circular dependencies from spatialmath import UnitQuaternion @@ -931,6 +931,29 @@ def angdist(self, other: SO3, metric: int = 6) -> Union[float, ndarray]: else: return ad + def mean(self, tol: float = 20) -> SO3: + """Mean of a set of rotations + + :param tol: iteration tolerance in units of eps, defaults to 20 + :type tol: float, optional + :return: the mean rotation + :rtype: :class:`SO3` instance. + + Computes the Karcher mean of the set of rotations within the SO(3) instance. + + :references: + - `**Hartley, Trumpf** - "Rotation Averaging" - IJCV 2011 `_, Algorithm 1, page 15. + - `Karcher mean `_ + """ + + eta = tol * np.finfo(float).eps + R_mean = self[0] # initial guess + while True: + r = np.dstack((R_mean.inv() * self).log()).mean(axis=2) + if np.linalg.norm(r) < eta: + return R_mean + R_mean = R_mean @ self.Exp(r) # update estimate and normalize + # ============================== SE3 =====================================# diff --git a/spatialmath/quaternion.py b/spatialmath/quaternion.py index 51561036..3633646b 100644 --- a/spatialmath/quaternion.py +++ b/spatialmath/quaternion.py @@ -45,10 +45,10 @@ def __init__(self, s: Any = None, v=None, check: Optional[bool] = True): r""" Construct a new quaternion - :param s: scalar - :type s: float - :param v: vector - :type v: 3-element array_like + :param s: scalar part + :type s: float or ndarray(N) + :param v: vector part + :type v: ndarray(3), ndarray(Nx3) - ``Quaternion()`` constructs a zero quaternion - ``Quaternion(s, v)`` construct a new quaternion from the scalar ``s`` @@ -78,7 +78,7 @@ def __init__(self, s: Any = None, v=None, check: Optional[bool] = True): super().__init__() if s is None and smb.isvector(v, 4): - v,s = (s,v) + v, s = (s, v) if v is None: # single argument @@ -92,6 +92,11 @@ def __init__(self, s: Any = None, v=None, check: Optional[bool] = True): # Quaternion(s, v) self.data = [np.r_[s, smb.getvector(v)]] + elif ( + smb.isvector(s) and smb.ismatrix(v, (None, 3)) and s.shape[0] == v.shape[0] + ): + # Quaternion(s, v) where s and v are arrays + self.data = [np.r_[_s, _v] for _s, _v in zip(s, v)] else: raise ValueError("bad argument to Quaternion constructor") @@ -395,9 +400,23 @@ def log(self) -> Quaternion: :seealso: :meth:`Quaternion.exp` :meth:`Quaternion.log` :meth:`UnitQuaternion.angvec` """ norm = self.norm() - s = math.log(norm) - v = math.acos(np.clip(self.s / norm, -1, 1)) * smb.unitvec(self.v) - return Quaternion(s=s, v=v) + s = np.log(norm) + if len(self) == 1: + if smb.iszerovec(self._A[1:4]): + v = np.zeros((3,)) + else: + v = math.acos(np.clip(self._A[0] / norm, -1, 1)) * smb.unitvec( + self._A[1:4] + ) + return Quaternion(s=s, v=v) + else: + v = [ + np.zeros((3,)) + if smb.iszerovec(A[1:4]) + else math.acos(np.clip(A[0] / n, -1, 1)) * smb.unitvec(A[1:4]) + for A, n in zip(self._A, norm) + ] + return Quaternion(s=s, v=np.array(v)) def exp(self, tol: float = 20) -> Quaternion: r""" @@ -437,7 +456,11 @@ def exp(self, tol: float = 20) -> Quaternion: exp_s = math.exp(self.s) norm_v = smb.norm(self.v) s = exp_s * math.cos(norm_v) - v = exp_s * self.v / norm_v * math.sin(norm_v) + if smb.iszerovec(self.v, tol * _eps): + # result will be a unit quaternion + v = np.zeros((3,)) + else: + v = exp_s * self.v / norm_v * math.sin(norm_v) if abs(self.s) < tol * _eps: # result will be a unit quaternion return UnitQuaternion(s=s, v=v) @@ -1260,7 +1283,7 @@ def Eul(cls, *angles: List[float], unit: Optional[str] = "rad") -> UnitQuaternio Construct a new unit quaternion from Euler angles :param 𝚪: 3-vector of Euler angles - :type 𝚪: array_like + :type 𝚪: 3 floats, array_like(3) or ndarray(N,3) :param unit: angular units: 'rad' [default], or 'deg' :type unit: str :return: unit-quaternion @@ -1286,12 +1309,15 @@ def Eul(cls, *angles: List[float], unit: Optional[str] = "rad") -> UnitQuaternio if len(angles) == 1: angles = angles[0] - return cls(smb.r2q(smb.eul2r(angles, unit=unit)), check=False) + if smb.isvector(angles, 3): + return cls(smb.r2q(smb.eul2r(angles, unit=unit)), check=False) + else: + return cls([smb.r2q(smb.eul2r(a, unit=unit)) for a in angles], check=False) @classmethod def RPY( cls, - *angles: List[float], + *angles, order: Optional[str] = "zyx", unit: Optional[str] = "rad", ) -> UnitQuaternion: @@ -1299,7 +1325,7 @@ def RPY( Construct a new unit quaternion from roll-pitch-yaw angles :param 𝚪: 3-vector of roll-pitch-yaw angles - :type 𝚪: array_like + :type 𝚪: 3 floats, array_like(3) or ndarray(N,3) :param unit: angular units: 'rad' [default], or 'deg' :type unit: str :param unit: rotation order: 'zyx' [default], 'xyz', or 'yxz' @@ -1341,7 +1367,13 @@ def RPY( if len(angles) == 1: angles = angles[0] - return cls(smb.r2q(smb.rpy2r(angles, unit=unit, order=order)), check=False) + if smb.isvector(angles, 3): + return cls(smb.r2q(smb.rpy2r(angles, unit=unit, order=order)), check=False) + else: + return cls( + [smb.r2q(smb.rpy2r(a, unit=unit, order=order)) for a in angles], + check=False, + ) @classmethod def OA(cls, o: ArrayLike3, a: ArrayLike3) -> UnitQuaternion: @@ -1569,6 +1601,24 @@ def dotb(self, omega: ArrayLike3) -> R4: """ return smb.qdotb(self._A, omega) + # def mean(self, tol: float = 20) -> SO3: + # """Mean of a set of rotations + + # :param tol: iteration tolerance in units of eps, defaults to 20 + # :type tol: float, optional + # :return: the mean rotation + # :rtype: :class:`UnitQuaternion` instance. + + # Computes the Karcher mean of the set of rotations within the unit quaternion instance. + + # :references: + # - `**Hartley, Trumpf** - "Rotation Averaging" - IJCV 2011 `_ + # - `Karcher mean UnitQuaternion: # lgtm[py/not-named-self] pylint: disable=no-self-argument diff --git a/tests/test_pose3d.py b/tests/test_pose3d.py index d6a941c3..ca03b70e 100755 --- a/tests/test_pose3d.py +++ b/tests/test_pose3d.py @@ -717,6 +717,37 @@ def test_functions_lie(self): nt.assert_equal(R, SO3.EulerVec(R.eulervec())) np.testing.assert_equal((R.inv() * R).eulervec(), np.zeros(3)) + R = SO3() # identity matrix case + + # Check log and exponential map + nt.assert_equal(R, SO3.Exp(R.log())) + np.testing.assert_equal((R.inv() * R).log(), np.zeros([3, 3])) + + # Check euler vector map + nt.assert_equal(R, SO3.EulerVec(R.eulervec())) + np.testing.assert_equal((R.inv() * R).eulervec(), np.zeros(3)) + + def test_mean(self): + rpy = np.ones((100, 1)) @ np.c_[0.1, 0.2, 0.3] + R = SO3.RPY(rpy) + self.assertEqual(len(R), 100) + m = R.mean() + self.assertIsInstance(m, SO3) + array_compare(m, R[0]) + + # range of angles, mean should be the middle one, index=25 + R = SO3.Rz(np.linspace(start=0.3, stop=0.7, num=51)) + m = R.mean() + self.assertIsInstance(m, SO3) + array_compare(m, R[25]) + + # now add noise + rng = np.random.default_rng(0) # reproducible random numbers + rpy += rng.normal(scale=0.00001, size=(100, 3)) + R = SO3.RPY(rpy) + m = R.mean() + array_compare(m, SO3.RPY(0.1, 0.2, 0.3)) + # ============================== SE3 =====================================# diff --git a/tests/test_quaternion.py b/tests/test_quaternion.py index 73c1b090..403b3c8d 100644 --- a/tests/test_quaternion.py +++ b/tests/test_quaternion.py @@ -257,18 +257,55 @@ def test_staticconstructors(self): UnitQuaternion.Rz(theta, "deg").R, rotz(theta, "deg") ) + def test_constructor_RPY(self): # 3 angle + q = UnitQuaternion.RPY([0.1, 0.2, 0.3]) + self.assertIsInstance(q, UnitQuaternion) + self.assertEqual(len(q), 1) + nt.assert_array_almost_equal(q.R, rpy2r(0.1, 0.2, 0.3)) + q = UnitQuaternion.RPY(0.1, 0.2, 0.3) + self.assertIsInstance(q, UnitQuaternion) + self.assertEqual(len(q), 1) + nt.assert_array_almost_equal(q.R, rpy2r(0.1, 0.2, 0.3)) + q = UnitQuaternion.RPY(np.r_[0.1, 0.2, 0.3]) + self.assertIsInstance(q, UnitQuaternion) + self.assertEqual(len(q), 1) + nt.assert_array_almost_equal(q.R, rpy2r(0.1, 0.2, 0.3)) + nt.assert_array_almost_equal( - UnitQuaternion.RPY([0.1, 0.2, 0.3]).R, rpy2r(0.1, 0.2, 0.3) + UnitQuaternion.RPY([10, 20, 30], unit="deg").R, + rpy2r(10, 20, 30, unit="deg"), ) - nt.assert_array_almost_equal( - UnitQuaternion.Eul([0.1, 0.2, 0.3]).R, eul2r(0.1, 0.2, 0.3) + UnitQuaternion.RPY([0.1, 0.2, 0.3], order="xyz").R, + rpy2r(0.1, 0.2, 0.3, order="xyz"), + ) + + angles = np.array( + [[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]] ) + q = UnitQuaternion.RPY(angles) + self.assertIsInstance(q, UnitQuaternion) + self.assertEqual(len(q), 4) + for i in range(4): + nt.assert_array_almost_equal(q[i].R, rpy2r(angles[i, :])) + + q = UnitQuaternion.RPY(angles, order="xyz") + self.assertIsInstance(q, UnitQuaternion) + self.assertEqual(len(q), 4) + for i in range(4): + nt.assert_array_almost_equal(q[i].R, rpy2r(angles[i, :], order="xyz")) + angles *= 10 + q = UnitQuaternion.RPY(angles, unit="deg") + self.assertIsInstance(q, UnitQuaternion) + self.assertEqual(len(q), 4) + for i in range(4): + nt.assert_array_almost_equal(q[i].R, rpy2r(angles[i, :], unit="deg")) + + def test_constructor_Eul(self): nt.assert_array_almost_equal( - UnitQuaternion.RPY([10, 20, 30], unit="deg").R, - rpy2r(10, 20, 30, unit="deg"), + UnitQuaternion.Eul([0.1, 0.2, 0.3]).R, eul2r(0.1, 0.2, 0.3) ) nt.assert_array_almost_equal( @@ -276,6 +313,23 @@ def test_staticconstructors(self): eul2r(10, 20, 30, unit="deg"), ) + angles = np.array( + [[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]] + ) + q = UnitQuaternion.Eul(angles) + self.assertIsInstance(q, UnitQuaternion) + self.assertEqual(len(q), 4) + for i in range(4): + nt.assert_array_almost_equal(q[i].R, eul2r(angles[i, :])) + + angles *= 10 + q = UnitQuaternion.Eul(angles, unit="deg") + self.assertIsInstance(q, UnitQuaternion) + self.assertEqual(len(q), 4) + for i in range(4): + nt.assert_array_almost_equal(q[i].R, eul2r(angles[i, :], unit="deg")) + + def test_constructor_AngVec(self): # (theta, v) th = 0.2 v = unitvec([1, 2, 3]) @@ -286,6 +340,7 @@ def test_staticconstructors(self): ) nt.assert_array_almost_equal(UnitQuaternion.AngVec(th, -v).R, angvec2r(th, -v)) + def test_constructor_EulerVec(self): # (theta, v) th = 0.2 v = unitvec([1, 2, 3]) @@ -830,6 +885,20 @@ def test_log(self): nt.assert_array_almost_equal(q1.log().exp(), q1) nt.assert_array_almost_equal(q2.log().exp(), q2) + q = Quaternion([q1, q2, q1, q2]) + qlog = q.log() + nt.assert_array_almost_equal(qlog[0].exp(), q1) + nt.assert_array_almost_equal(qlog[1].exp(), q2) + nt.assert_array_almost_equal(qlog[2].exp(), q1) + nt.assert_array_almost_equal(qlog[3].exp(), q2) + + q = UnitQuaternion() # identity + qlog = q.log() + nt.assert_array_almost_equal(qlog.vec, np.zeros(4)) + qq = qlog.exp() + self.assertIsInstance(qq, UnitQuaternion) + nt.assert_array_almost_equal(qq.vec, np.r_[1, 0, 0, 0]) + def test_concat(self): u = Quaternion() uu = Quaternion([u, u, u, u]) @@ -1018,6 +1087,27 @@ def test_miscellany(self): nt.assert_equal(q.inner(q), q.norm() ** 2) nt.assert_equal(q.inner(u), np.dot(q.vec, u.vec)) + # def test_mean(self): + # rpy = np.ones((100, 1)) @ np.c_[0.1, 0.2, 0.3] + # q = UnitQuaternion.RPY(rpy) + # self.assertEqual(len(q), 100) + # m = q.mean() + # self.assertIsInstance(m, UnitQuaternion) + # nt.assert_array_almost_equal(m.vec, q[0].vec) + + # # range of angles, mean should be the middle one, index=25 + # q = UnitQuaternion.Rz(np.linspace(start=0.3, stop=0.7, num=51)) + # m = q.mean() + # self.assertIsInstance(m, UnitQuaternion) + # nt.assert_array_almost_equal(m.vec, q[25].vec) + + # # now add noise + # rng = np.random.default_rng(0) # reproducible random numbers + # rpy += rng.normal(scale=0.1, size=(100, 3)) + # q = UnitQuaternion.RPY(rpy) + # m = q.mean() + # nt.assert_array_almost_equal(m.vec, q.RPY(0.1, 0.2, 0.3).vec) + # ---------------------------------------------------------------------------------------# if __name__ == "__main__": From 8c6d42262941de10e453237cc39940f9042e27ca Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Sun, 26 Jan 2025 17:00:26 +1100 Subject: [PATCH 07/14] Add methods to convert SE3 to/from the rvec, tvec format used by OpenCV (#157) --- spatialmath/pose3d.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_pose3d.py | 11 ++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/spatialmath/pose3d.py b/spatialmath/pose3d.py index 4e558512..b42d752c 100644 --- a/spatialmath/pose3d.py +++ b/spatialmath/pose3d.py @@ -1315,6 +1315,21 @@ def delta(self, X2: Optional[SE3] = None) -> R6: else: return smb.tr2delta(self.A, X2.A) + def rtvec(self) -> Tuple[R3, R3]: + """ + Convert to OpenCV-style rotation and translation vectors + + :return: rotation and translation vectors + :rtype: ndarray(3), ndarray(3) + + Many OpenCV functions accept pose as two 3-vectors: a rotation vector using + exponential coordinates and a translation vector. This method combines them + into an SE(3) instance. + + :seealso: :meth:`rtvec` + """ + return SO3(self).log(twist=True), self.t + def Ad(self) -> R6x6: r""" Adjoint of SE(3) @@ -1856,6 +1871,26 @@ def Exp(cls, S: Union[R6, R4x4], check: bool = True) -> SE3: else: return cls(smb.trexp(S), check=False) + @classmethod + def RTvec(cls, rvec: ArrayLike3, tvec: ArrayLike3) -> Self: + """ + Construct a new SE(3) from OpenCV-style rotation and translation vectors + + :param rvec: rotation as exponential coordinates + :type rvec: ArrayLike3 + :param tvec: translation vector + :type tvec: ArrayLike3 + :return: An SE(3) instance + :rtype: SE3 instance + + Many OpenCV functions (such as pose estimation) return pose as two 3-vectors: a + rotation vector using exponential coordinates and a translation vector. This + method combines them into an SE(3) instance. + + :seealso: :meth:`rtvec` + """ + return SE3.Rt(smb.trexp(rvec), tvec) + @classmethod def Delta(cls, d: ArrayLike6) -> SE3: r""" diff --git a/tests/test_pose3d.py b/tests/test_pose3d.py index ca03b70e..86d4c414 100755 --- a/tests/test_pose3d.py +++ b/tests/test_pose3d.py @@ -254,7 +254,6 @@ def test_conversion(self): nt.assert_array_almost_equal(q.SO3(), R_from_q) nt.assert_array_almost_equal(q.SO3().UnitQuaternion(), q) - def test_shape(self): a = SO3() self.assertEqual(a._A.shape, a.shape) @@ -1370,6 +1369,16 @@ def test_functions_vect(self): # .T pass + def test_rtvec(self): + # OpenCV compatibility functions + T = SE3.RTvec([0, 1, 0], [2, 3, 4]) + nt.assert_equal(T.t, [2, 3, 4]) + nt.assert_equal(T.R, SO3.Ry(1)) + + rvec, tvec = T.rtvec() + nt.assert_equal(rvec, [0, 1, 0]) + nt.assert_equal(tvec, [2, 3, 4]) + # ---------------------------------------------------------------------------------------# if __name__ == "__main__": From a08f25ed97d46db6bd5098d76797164e571f7702 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Sun, 26 Jan 2025 17:02:17 +1100 Subject: [PATCH 08/14] Add `q2str` to convert quaternion to string (#158) --- spatialmath/base/__init__.py | 1 + spatialmath/base/quaternions.py | 65 ++++++++++++++++++++++++++------- spatialmath/quaternion.py | 2 +- tests/base/test_quaternions.py | 36 ++++++++++++------ 4 files changed, 79 insertions(+), 25 deletions(-) diff --git a/spatialmath/base/__init__.py b/spatialmath/base/__init__.py index 98ff87f0..9e9fbcbe 100644 --- a/spatialmath/base/__init__.py +++ b/spatialmath/base/__init__.py @@ -208,6 +208,7 @@ "qdotb", "qangle", "qprint", + "q2str", # spatialmath.base.transforms2d "rot2", "trot2", diff --git a/spatialmath/base/quaternions.py b/spatialmath/base/quaternions.py index 8f33bc1c..d5652d4a 100755 --- a/spatialmath/base/quaternions.py +++ b/spatialmath/base/quaternions.py @@ -19,9 +19,11 @@ import scipy.interpolate as interpolate from typing import Optional from functools import lru_cache +import warnings _eps = np.finfo(np.float64).eps + def qeye() -> QuaternionArray: """ Create an identity quaternion @@ -56,7 +58,7 @@ def qpure(v: ArrayLike3) -> QuaternionArray: .. runblock:: pycon - >>> from spatialmath.base import pure, qprint + >>> from spatialmath.base import qpure, qprint >>> q = qpure([1, 2, 3]) >>> qprint(q) """ @@ -1088,14 +1090,53 @@ def qangle(q1: ArrayLike4, q2: ArrayLike4) -> float: return 4.0 * math.atan2(smb.norm(q1 - q2), smb.norm(q1 + q2)) +def q2str( + q: Union[ArrayLike4, ArrayLike4], + delim: Optional[Tuple[str, str]] = ("<", ">"), + fmt: Optional[str] = "{: .4f}", +) -> str: + """ + Format a quaternion as a string + + :arg q: unit-quaternion + :type q: array_like(4) + :arg delim: 2-list of delimeters [default ('<', '>')] + :type delim: list or tuple of strings + :arg fmt: printf-style format soecifier [default '{: .4f}'] + :type fmt: str + :return: formatted string + :rtype: str + + Format the quaternion in a human-readable form as:: + + S D1 VX VY VZ D2 + + where S, VX, VY, VZ are the quaternion elements, and D1 and D2 are a pair + of delimeters given by `delim`. + + .. runblock:: pycon + + >>> from spatialmath.base import q2str, qrand + >>> q = [1, 2, 3, 4] + >>> q2str(q) + >>> q = qrand() # a unit quaternion + >>> q2str(q, delim=('<<', '>>')) + + :seealso: :meth:`qprint` + """ + q = smb.getvector(q, 4) + template = "# {} #, #, # {}".replace("#", fmt) + return template.format(q[0], delim[0], q[1], q[2], q[3], delim[1]) + + def qprint( q: Union[ArrayLike4, ArrayLike4], delim: Optional[Tuple[str, str]] = ("<", ">"), fmt: Optional[str] = "{: .4f}", file: Optional[TextIO] = sys.stdout, -) -> str: +) -> None: """ - Format a quaternion + Format a quaternion to a file :arg q: unit-quaternion :type q: array_like(4) @@ -1105,8 +1146,6 @@ def qprint( :type fmt: str :arg file: destination for formatted string [default sys.stdout] :type file: file object - :return: formatted string - :rtype: str Format the quaternion in a human-readable form as:: @@ -1117,8 +1156,6 @@ def qprint( By default the string is written to `sys.stdout`. - If `file=None` then a string is returned. - .. runblock:: pycon >>> from spatialmath.base import qprint, qrand @@ -1126,14 +1163,16 @@ def qprint( >>> qprint(q) >>> q = qrand() # a unit quaternion >>> qprint(q, delim=('<<', '>>')) + + :seealso: :meth:`q2str` """ q = smb.getvector(q, 4) - template = "# {} #, #, # {}".replace("#", fmt) - s = template.format(q[0], delim[0], q[1], q[2], q[3], delim[1]) - if file: - file.write(s + "\n") - else: - return s + if file is None: + warnings.warn( + "Usage: qprint(..., file=None) -> str is deprecated, use q2str() instead", + DeprecationWarning, + ) + print(q2str(q, delim=delim, fmt=fmt), file=file) if __name__ == "__main__": # pragma: no cover diff --git a/spatialmath/quaternion.py b/spatialmath/quaternion.py index 3633646b..f9b73873 100644 --- a/spatialmath/quaternion.py +++ b/spatialmath/quaternion.py @@ -920,7 +920,7 @@ def __str__(self) -> str: delim = ("<<", ">>") else: delim = ("<", ">") - return "\n".join([smb.qprint(q, file=None, delim=delim) for q in self.data]) + return "\n".join([smb.q2str(q, delim=delim) for q in self.data]) # ========================================================================= # diff --git a/tests/base/test_quaternions.py b/tests/base/test_quaternions.py index c512c6d2..f5859b54 100644 --- a/tests/base/test_quaternions.py +++ b/tests/base/test_quaternions.py @@ -36,6 +36,7 @@ import spatialmath.base as tr from spatialmath.base.quaternions import * import spatialmath as sm +import io class TestQuaternion(unittest.TestCase): @@ -96,19 +97,32 @@ def test_ops(self): ), True, ) + nt.assert_equal(isunitvec(qrand()), True) - s = qprint(np.r_[1, 1, 0, 0], file=None) - nt.assert_equal(isinstance(s, str), True) - nt.assert_equal(len(s) > 2, True) - s = qprint([1, 1, 0, 0], file=None) + def test_display(self): + s = q2str(np.r_[1, 2, 3, 4]) nt.assert_equal(isinstance(s, str), True) - nt.assert_equal(len(s) > 2, True) + nt.assert_equal(s, " 1.0000 < 2.0000, 3.0000, 4.0000 >") + + s = q2str([1, 2, 3, 4]) + nt.assert_equal(s, " 1.0000 < 2.0000, 3.0000, 4.0000 >") + s = q2str([1, 2, 3, 4], delim=("<<", ">>")) + nt.assert_equal(s, " 1.0000 << 2.0000, 3.0000, 4.0000 >>") + + s = q2str([1, 2, 3, 4], fmt="{:20.6f}") nt.assert_equal( - qprint([1, 2, 3, 4], file=None), " 1.0000 < 2.0000, 3.0000, 4.0000 >" + s, + " 1.000000 < 2.000000, 3.000000, 4.000000 >", ) - nt.assert_equal(isunitvec(qrand()), True) + # would be nicer to do this with redirect_stdout() from contextlib but that + # fails because file=sys.stdout is maybe assigned at compile time, so when + # contextlib changes sys.stdout, qprint() doesn't see it + + f = io.StringIO() + qprint(np.r_[1, 2, 3, 4], file=f) + nt.assert_equal(f.getvalue().rstrip(), " 1.0000 < 2.0000, 3.0000, 4.0000 >") def test_rotation(self): # rotation matrix to quaternion @@ -227,12 +241,12 @@ def test_r2q(self): def test_qangle(self): # Test function that calculates angle between quaternions - q1 = [1., 0, 0, 0] - q2 = [1 / np.sqrt(2), 0, 1 / np.sqrt(2), 0] # 90deg rotation about y-axis + q1 = [1.0, 0, 0, 0] + q2 = [1 / np.sqrt(2), 0, 1 / np.sqrt(2), 0] # 90deg rotation about y-axis nt.assert_almost_equal(qangle(q1, q2), np.pi / 2) - q1 = [1., 0, 0, 0] - q2 = [1 / np.sqrt(2), 1 / np.sqrt(2), 0, 0] # 90deg rotation about x-axis + q1 = [1.0, 0, 0, 0] + q2 = [1 / np.sqrt(2), 1 / np.sqrt(2), 0, 0] # 90deg rotation about x-axis nt.assert_almost_equal(qangle(q1, q2), np.pi / 2) From 659ba24a80cca2d330b983a80b7f62ca66a262ce Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Fri, 31 Jan 2025 04:26:47 +1100 Subject: [PATCH 09/14] new method to construct an SO3 object (#159) Co-authored-by: Mark Yeatman <129521731+myeatman-bdai@users.noreply.github.com> --- spatialmath/pose3d.py | 42 ++++++++++++++++++++++++++++++++++++++++++ tests/test_pose3d.py | 12 ++++++++++++ 2 files changed, 54 insertions(+) diff --git a/spatialmath/pose3d.py b/spatialmath/pose3d.py index b42d752c..3b4821e5 100644 --- a/spatialmath/pose3d.py +++ b/spatialmath/pose3d.py @@ -725,6 +725,48 @@ def vval(v): return cls(np.c_[x, y, z], check=True) + @classmethod + def RotatedVector(cls, v1: ArrayLike3, v2: ArrayLike3, tol=20) -> Self: + """ + Construct a new SO(3) from a vector and its rotated image + + :param v1: initial vector + :type v1: array_like(3) + :param v2: vector after rotation + :type v2: array_like(3) + :param tol: tolerance for singularity in units of eps, defaults to 20 + :type tol: float + :return: SO(3) rotation + :rtype: :class:`SO3` instance + + ``SO3.RotatedVector(v1, v2)`` is an SO(3) rotation defined in terms of + two vectors. The rotation takes vector ``v1`` to ``v2``. + + .. runblock:: pycon + + >>> from spatialmath import SO3 + >>> v1 = [1, 2, 3] + >>> v2 = SO3.Eul(0.3, 0.4, 0.5) * v1 + >>> print(v2) + >>> R = SO3.RotatedVector(v1, v2) + >>> print(R) + >>> print(R * v1) + + .. note:: The vectors do not have to be unit-length. + """ + # https://math.stackexchange.com/questions/180418/calculate-rotation-matrix-to-align-vector-a-to-vector-b-in-3d + v1 = smb.unitvec(v1) + v2 = smb.unitvec(v2) + v = smb.cross(v1, v2) + s = smb.norm(v) + if abs(s) < tol * np.finfo(float).eps: + return cls(np.eye(3), check=False) + else: + c = np.dot(v1, v2) + V = smb.skew(v) + R = np.eye(3) + V + V @ V * (1 - c) / (s**2) + return cls(R, check=False) + @classmethod def AngleAxis(cls, theta: float, v: ArrayLike3, *, unit: str = "rad") -> Self: r""" diff --git a/tests/test_pose3d.py b/tests/test_pose3d.py index 86d4c414..58396441 100755 --- a/tests/test_pose3d.py +++ b/tests/test_pose3d.py @@ -716,6 +716,17 @@ def test_functions_lie(self): nt.assert_equal(R, SO3.EulerVec(R.eulervec())) np.testing.assert_equal((R.inv() * R).eulervec(), np.zeros(3)) + + def test_rotatedvector(self): + v1 = [1, 2, 3] + R = SO3.Eul(0.3, 0.4, 0.5) + v2 = R * v1 + Re = SO3.RotatedVector(v1, v2) + np.testing.assert_almost_equal(v2, Re * v1) + + Re = SO3.RotatedVector(v1, v1) + np.testing.assert_almost_equal(Re, np.eye(3)) + R = SO3() # identity matrix case # Check log and exponential map @@ -748,6 +759,7 @@ def test_mean(self): array_compare(m, SO3.RPY(0.1, 0.2, 0.3)) + # ============================== SE3 =====================================# From 39d6127465432823a386deae2d7fa356de1125e2 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Fri, 31 Jan 2025 04:27:13 +1100 Subject: [PATCH 10/14] Fix for issue #144. (#155) --- spatialmath/base/argcheck.py | 50 ++++++++++++++++++++++---------- spatialmath/base/transforms2d.py | 2 +- spatialmath/base/transforms3d.py | 35 +++++++++++++--------- spatialmath/base/vectors.py | 18 ++++++++---- spatialmath/quaternion.py | 2 +- tests/base/test_argcheck.py | 42 +++++++++++++++++++++++++-- 6 files changed, 109 insertions(+), 40 deletions(-) diff --git a/spatialmath/base/argcheck.py b/spatialmath/base/argcheck.py index 40f94336..38b5eb1a 100644 --- a/spatialmath/base/argcheck.py +++ b/spatialmath/base/argcheck.py @@ -522,7 +522,9 @@ def isvector(v: Any, dim: Optional[int] = None) -> bool: return False -def getunit(v: ArrayLike, unit: str = "rad", dim=None) -> Union[float, NDArray]: +def getunit( + v: ArrayLike, unit: str = "rad", dim: Optional[int] = None, vector: bool = True +) -> Union[float, NDArray]: """ Convert values according to angular units @@ -530,8 +532,10 @@ def getunit(v: ArrayLike, unit: str = "rad", dim=None) -> Union[float, NDArray]: :type v: array_like(m) :param unit: the angular unit, "rad" or "deg" :type unit: str - :param dim: expected dimension of input, defaults to None + :param dim: expected dimension of input, defaults to don't check (None) :type dim: int, optional + :param vector: return a scalar as a 1d vector, defaults to True + :type vector: bool, optional :return: the converted value in radians :rtype: ndarray(m) or float :raises ValueError: argument is not a valid angular unit @@ -543,30 +547,44 @@ def getunit(v: ArrayLike, unit: str = "rad", dim=None) -> Union[float, NDArray]: >>> from spatialmath.base import getunit >>> import numpy as np >>> getunit(1.5, 'rad') - >>> getunit(1.5, 'rad', dim=0) - >>> # getunit([1.5], 'rad', dim=0) --> ValueError >>> getunit(90, 'deg') + >>> getunit(90, 'deg', vector=False) # force a scalar output + >>> getunit(1.5, 'rad', dim=0) # check argument is scalar + >>> getunit(1.5, 'rad', dim=3) # check argument is a 3-vector + >>> getunit([1.5], 'rad', dim=1) # check argument is a 1-vector + >>> getunit([1.5], 'rad', dim=3) # check argument is a 3-vector >>> getunit([90, 180], 'deg') - >>> getunit(np.r_[0.5, 1], 'rad') >>> getunit(np.r_[90, 180], 'deg') - >>> getunit(np.r_[90, 180], 'deg', dim=2) - >>> # getunit([90, 180], 'deg', dim=3) --> ValueError + >>> getunit(np.r_[90, 180], 'deg', dim=2) # check argument is a 2-vector + >>> getunit([90, 180], 'deg', dim=3) # check argument is a 3-vector :note: - the input value is processed by :func:`getvector` and the argument ``dim`` can - be used to check that ``v`` is the desired length. - - the output is always an ndarray except if the input is a scalar and ``dim=0``. + be used to check that ``v`` is the desired length. Note that 0 means a scalar, + whereas 1 means a 1-element array. + - the output is always an ndarray except if the input is a scalar and ``vector=False``. :seealso: :func:`getvector` """ - if not isinstance(v, Iterable) and dim == 0: - # scalar in, scalar out - if unit == "rad": - return v - elif unit == "deg": - return np.deg2rad(v) + if not isinstance(v, Iterable): + # scalar input + if dim is not None and dim != 0: + raise ValueError("for dim==0 input must be a scalar") + if vector: + # scalar in, vector out + if unit == "deg": + v = np.deg2rad(v) + elif unit != "rad": + raise ValueError("invalid angular units") + return np.array([v]) else: - raise ValueError("invalid angular units") + # scalar in, scalar out + if unit == "rad": + return v + elif unit == "deg": + return np.deg2rad(v) + else: + raise ValueError("invalid angular units") else: # scalar or iterable in, ndarray out diff --git a/spatialmath/base/transforms2d.py b/spatialmath/base/transforms2d.py index 682ea0ca..25265fff 100644 --- a/spatialmath/base/transforms2d.py +++ b/spatialmath/base/transforms2d.py @@ -63,7 +63,7 @@ def rot2(theta: float, unit: str = "rad") -> SO2Array: >>> rot2(0.3) >>> rot2(45, 'deg') """ - theta = smb.getunit(theta, unit, dim=0) + theta = smb.getunit(theta, unit, vector=False) ct = smb.sym.cos(theta) st = smb.sym.sin(theta) # fmt: off diff --git a/spatialmath/base/transforms3d.py b/spatialmath/base/transforms3d.py index 3617f965..350e1d14 100644 --- a/spatialmath/base/transforms3d.py +++ b/spatialmath/base/transforms3d.py @@ -79,7 +79,7 @@ def rotx(theta: float, unit: str = "rad") -> SO3Array: :SymPy: supported """ - theta = getunit(theta, unit, dim=0) + theta = getunit(theta, unit, vector=False) ct = sym.cos(theta) st = sym.sin(theta) # fmt: off @@ -118,7 +118,7 @@ def roty(theta: float, unit: str = "rad") -> SO3Array: :SymPy: supported """ - theta = getunit(theta, unit, dim=0) + theta = getunit(theta, unit, vector=False) ct = sym.cos(theta) st = sym.sin(theta) # fmt: off @@ -152,7 +152,7 @@ def rotz(theta: float, unit: str = "rad") -> SO3Array: :seealso: :func:`~trotz` :SymPy: supported """ - theta = getunit(theta, unit, dim=0) + theta = getunit(theta, unit, vector=False) ct = sym.cos(theta) st = sym.sin(theta) # fmt: off @@ -2709,7 +2709,7 @@ def tr2adjoint(T): :Reference: - Robotics, Vision & Control for Python, Section 3, P. Corke, Springer 2023. - - `Lie groups for 2D and 3D Transformations _ + - `Lie groups for 2D and 3D Transformations `_ :SymPy: supported """ @@ -3002,29 +3002,36 @@ def trplot( - ``width`` of line - ``length`` of line - ``style`` which is one of: + - ``'arrow'`` [default], draw line with arrow head in ``color`` - ``'line'``, draw line with no arrow head in ``color`` - ``'rgb'``, frame axes are lines with no arrow head and red for X, green - for Y, blue for Z; no origin dot + for Y, blue for Z; no origin dot - ``'rviz'``, frame axes are thick lines with no arrow head and red for X, - green for Y, blue for Z; no origin dot + green for Y, blue for Z; no origin dot + - coordinate axis labels depend on: + - ``axislabel`` if True [default] label the axis, default labels are X, Y, Z - ``labels`` 3-list of alternative axis labels - ``textcolor`` which defaults to ``color`` - ``axissubscript`` if True [default] add the frame label ``frame`` as a subscript - for each axis label + for each axis label + - coordinate frame label depends on: + - `frame` the label placed inside {} near the origin of the frame + - a dot at the origin + - ``originsize`` size of the dot, if zero no dot - ``origincolor`` color of the dot, defaults to ``color`` Examples:: - trplot(T, frame='A') - trplot(T, frame='A', color='green') - trplot(T1, 'labels', 'UVW'); + trplot(T, frame='A') + trplot(T, frame='A', color='green') + trplot(T1, 'labels', 'UVW'); .. plot:: @@ -3383,12 +3390,12 @@ def tranimate(T: Union[SO3Array, SE3Array], **kwargs) -> str: :param **kwargs: arguments passed to ``trplot`` - ``tranimate(T)`` where ``T`` is an SO(3) or SE(3) matrix, animates a 3D - coordinate frame moving from the world frame to the frame ``T`` in - ``nsteps``. + coordinate frame moving from the world frame to the frame ``T`` in + ``nsteps``. - ``tranimate(I)`` where ``I`` is an iterable or generator, animates a 3D - coordinate frame representing the pose of each element in the sequence of - SO(3) or SE(3) matrices. + coordinate frame representing the pose of each element in the sequence of + SO(3) or SE(3) matrices. Examples: diff --git a/spatialmath/base/vectors.py b/spatialmath/base/vectors.py index f29740a3..bf95283f 100644 --- a/spatialmath/base/vectors.py +++ b/spatialmath/base/vectors.py @@ -530,6 +530,7 @@ def wrap_0_pi(theta: ArrayLike) -> Union[float, NDArray]: :param theta: input angle :type theta: scalar or ndarray :return: angle wrapped into range :math:`[0, \pi)` + :rtype: scalar or ndarray This is used to fold angles of colatitude. If zero is the angle of the north pole, colatitude increases to :math:`\pi` at the south pole then @@ -537,7 +538,7 @@ def wrap_0_pi(theta: ArrayLike) -> Union[float, NDArray]: :seealso: :func:`wrap_mpi2_pi2` :func:`wrap_0_2pi` :func:`wrap_mpi_pi` :func:`angle_wrap` """ - theta = np.abs(theta) + theta = np.abs(getvector(theta)) n = theta / np.pi if isinstance(n, np.ndarray): n = n.astype(int) @@ -546,7 +547,7 @@ def wrap_0_pi(theta: ArrayLike) -> Union[float, NDArray]: y = np.where(np.bitwise_and(n, 1) == 0, theta - n * np.pi, (n + 1) * np.pi - theta) if isinstance(y, np.ndarray) and y.size == 1: - return float(y) + return float(y[0]) else: return y @@ -558,6 +559,7 @@ def wrap_mpi2_pi2(theta: ArrayLike) -> Union[float, NDArray]: :param theta: input angle :type theta: scalar or ndarray :return: angle wrapped into range :math:`[-\pi/2, \pi/2]` + :rtype: scalar or ndarray This is used to fold angles of latitude. @@ -573,7 +575,7 @@ def wrap_mpi2_pi2(theta: ArrayLike) -> Union[float, NDArray]: y = np.where(np.bitwise_and(n, 1) == 0, theta - n * np.pi, n * np.pi - theta) if isinstance(y, np.ndarray) and len(y) == 1: - return float(y) + return float(y[0]) else: return y @@ -585,13 +587,14 @@ def wrap_0_2pi(theta: ArrayLike) -> Union[float, NDArray]: :param theta: input angle :type theta: scalar or ndarray :return: angle wrapped into range :math:`[0, 2\pi)` + :rtype: scalar or ndarray :seealso: :func:`wrap_mpi_pi` :func:`wrap_0_pi` :func:`wrap_mpi2_pi2` :func:`angle_wrap` """ theta = getvector(theta) y = theta - 2.0 * math.pi * np.floor(theta / 2.0 / np.pi) if isinstance(y, np.ndarray) and len(y) == 1: - return float(y) + return float(y[0]) else: return y @@ -603,13 +606,14 @@ def wrap_mpi_pi(theta: ArrayLike) -> Union[float, NDArray]: :param theta: input angle :type theta: scalar or ndarray :return: angle wrapped into range :math:`[-\pi, \pi)` + :rtype: scalar or ndarray :seealso: :func:`wrap_0_2pi` :func:`wrap_0_pi` :func:`wrap_mpi2_pi2` :func:`angle_wrap` """ theta = getvector(theta) y = np.mod(theta + math.pi, 2 * math.pi) - np.pi if isinstance(y, np.ndarray) and len(y) == 1: - return float(y) + return float(y[0]) else: return y @@ -643,6 +647,7 @@ def angdiff(a, b=None): - ``angdiff(a, b)`` is the difference ``a - b`` wrapped to the range :math:`[-\pi, \pi)`. This is the operator :math:`a \circleddash b` used in the RVC book + - If ``a`` and ``b`` are both scalars, the result is scalar - If ``a`` is array_like, the result is a NumPy array ``a[i]-b`` - If ``a`` is array_like, the result is a NumPy array ``a-b[i]`` @@ -651,6 +656,7 @@ def angdiff(a, b=None): - ``angdiff(a)`` is the angle or vector of angles ``a`` wrapped to the range :math:`[-\pi, \pi)`. + - If ``a`` is a scalar, the result is scalar - If ``a`` is array_like, the result is a NumPy array @@ -671,7 +677,7 @@ def angdiff(a, b=None): y = np.mod(a + math.pi, 2 * math.pi) - math.pi if isinstance(y, np.ndarray) and len(y) == 1: - return float(y) + return float(y[0]) else: return y diff --git a/spatialmath/quaternion.py b/spatialmath/quaternion.py index f9b73873..79bdaf0c 100644 --- a/spatialmath/quaternion.py +++ b/spatialmath/quaternion.py @@ -1443,7 +1443,7 @@ def AngVec( :seealso: :meth:`UnitQuaternion.angvec` :meth:`UnitQuaternion.exp` :func:`~spatialmath.base.transforms3d.angvec2r` """ v = smb.getvector(v, 3) - theta = smb.getunit(theta, unit, dim=0) + theta = smb.getunit(theta, unit, vector=False) return cls( s=math.cos(theta / 2), v=math.sin(theta / 2) * v, norm=False, check=False ) diff --git a/tests/base/test_argcheck.py b/tests/base/test_argcheck.py index 685393b5..39c943d1 100755 --- a/tests/base/test_argcheck.py +++ b/tests/base/test_argcheck.py @@ -122,11 +122,49 @@ def test_verifymatrix(self): verifymatrix(a, (3, 4)) def test_unit(self): - self.assertIsInstance(getunit(1), np.ndarray) + # scalar -> vector + self.assertEqual(getunit(1), np.array([1])) + self.assertEqual(getunit(1, dim=0), np.array([1])) + with self.assertRaises(ValueError): + self.assertEqual(getunit(1, dim=1), np.array([1])) + + self.assertEqual(getunit(1, unit="deg"), np.array([1 * math.pi / 180.0])) + self.assertEqual(getunit(1, dim=0, unit="deg"), np.array([1 * math.pi / 180.0])) + with self.assertRaises(ValueError): + self.assertEqual( + getunit(1, dim=1, unit="deg"), np.array([1 * math.pi / 180.0]) + ) + + # scalar -> scalar + self.assertEqual(getunit(1, vector=False), 1) + self.assertEqual(getunit(1, dim=0, vector=False), 1) + with self.assertRaises(ValueError): + self.assertEqual(getunit(1, dim=1, vector=False), 1) + + self.assertIsInstance(getunit(1.0, vector=False), float) + self.assertIsInstance(getunit(1, vector=False), int) + + self.assertEqual(getunit(1, vector=False, unit="deg"), 1 * math.pi / 180.0) + self.assertEqual( + getunit(1, dim=0, vector=False, unit="deg"), 1 * math.pi / 180.0 + ) + with self.assertRaises(ValueError): + self.assertEqual( + getunit(1, dim=1, vector=False, unit="deg"), 1 * math.pi / 180.0 + ) + + self.assertIsInstance(getunit(1.0, vector=False, unit="deg"), float) + self.assertIsInstance(getunit(1, vector=False, unit="deg"), float) + + # vector -> vector + self.assertEqual(getunit([1]), np.array([1])) + self.assertEqual(getunit([1], dim=1), np.array([1])) + with self.assertRaises(ValueError): + getunit([1], dim=0) + self.assertIsInstance(getunit([1, 2]), np.ndarray) self.assertIsInstance(getunit((1, 2)), np.ndarray) self.assertIsInstance(getunit(np.r_[1, 2]), np.ndarray) - self.assertIsInstance(getunit(1.0, dim=0), float) nt.assert_equal(getunit(5, "rad"), 5) nt.assert_equal(getunit(5, "deg"), 5 * math.pi / 180.0) From d1057e56109ef37a1987b0031e593fd0428f8af6 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Fri, 31 Jan 2025 04:27:37 +1100 Subject: [PATCH 11/14] Fix the intersphinx source for matplotlib. (#156) --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 369c185b..b039bc85 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -187,7 +187,7 @@ intersphinx_mapping = { "numpy": ("http://docs.scipy.org/doc/numpy/", None), "scipy": ("http://docs.scipy.org/doc/scipy/reference/", None), - "matplotlib": ("http://matplotlib.sourceforge.net/", None), + "matplotlib": ("https://matplotlib.org/stable/", None), } # -------- Options favicon -------------------------------------------------------# From c04abc701d79d738826643938421d883f94a3e64 Mon Sep 17 00:00:00 2001 From: Mark Yeatman <129521731+myeatman-bdai@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:12:15 -0500 Subject: [PATCH 12/14] Format everything with black. (#161) --- .pre-commit-config.yaml | 35 ++++ docs/source/conf.py | 10 +- pyproject.toml | 9 +- spatialmath/DualQuaternion.py | 1 - spatialmath/__init__.py | 4 +- spatialmath/base/animate.py | 19 ++- spatialmath/base/quaternions.py | 75 ++++++--- spatialmath/base/transforms2d.py | 12 +- spatialmath/base/transforms3d.py | 25 +-- spatialmath/baseposelist.py | 5 +- spatialmath/baseposematrix.py | 19 ++- spatialmath/pose3d.py | 33 ++-- spatialmath/quaternion.py | 15 +- spatialmath/spline.py | 22 +-- spatialmath/twist.py | 2 +- tests/base/test_numeric.py | 9 - tests/base/test_symbolic.py | 5 +- tests/base/test_transforms.py | 9 - tests/base/test_transforms2d.py | 19 +-- tests/base/test_transforms3d.py | 6 +- tests/base/test_transformsNd.py | 5 +- tests/base/test_vectors.py | 49 +++--- tests/test_baseposelist.py | 22 +-- tests/test_dualquaternion.py | 59 +++---- tests/test_pose3d.py | 8 +- tests/test_quaternion.py | 12 +- tests/test_spline.py | 45 +++-- tests/test_twist.py | 274 ++++++++++++++++--------------- 28 files changed, 437 insertions(+), 371 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..36085fe2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +repos: +# - repo: https://github.com/charliermarsh/ruff-pre-commit +# # Ruff version. +# rev: 'v0.1.0' +# hooks: +# - id: ruff +# args: ['--fix', '--config', 'pyproject.toml'] + +- repo: https://github.com/psf/black + rev: 'refs/tags/23.10.0:refs/tags/23.10.0' + hooks: + - id: black + language_version: python3.10 + args: ['--config', 'pyproject.toml'] + verbose: true + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: end-of-file-fixer + - id: debug-statements # Ensure we don't commit `import pdb; pdb.set_trace()` + exclude: | + (?x)^( + docker/ros/web/static/.*| + )$ + - id: trailing-whitespace + exclude: | + (?x)^( + docker/ros/web/static/.*| + (.*/).*\.patch| + )$ +# - repo: https://github.com/pre-commit/mirrors-mypy +# rev: v1.6.1 +# hooks: +# - id: mypy diff --git a/docs/source/conf.py b/docs/source/conf.py index b039bc85..b5fcf84a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,8 +11,6 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import os -import sys # sys.path.insert(0, os.path.abspath('.')) # sys.path.insert(0, os.path.abspath('..')) @@ -67,9 +65,11 @@ # choose UTF-8 encoding to allow for Unicode characters, eg. ansitable # Python session setup, turn off color printing for SE3, set NumPy precision autorun_languages = {} -autorun_languages['pycon_output_encoding'] = 'UTF-8' -autorun_languages['pycon_input_encoding'] = 'UTF-8' -autorun_languages['pycon_runfirst'] = """ +autorun_languages["pycon_output_encoding"] = "UTF-8" +autorun_languages["pycon_input_encoding"] = "UTF-8" +autorun_languages[ + "pycon_runfirst" +] = """ from spatialmath import SE3 SE3._color = False import numpy as np diff --git a/pyproject.toml b/pyproject.toml index 4295f2f3..452bf290 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ keywords = [ "SO(2)", "SE(2)", "SO(3)", "SE(3)", "twist", "product of exponential", "translation", "orientation", "angle-axis", "Lie group", "skew symmetric matrix", - "pose", "translation", "rotation matrix", + "pose", "translation", "rotation matrix", "rigid body transform", "homogeneous transformation", "Euler angles", "roll-pitch-yaw angles", "quaternion", "unit-quaternion", @@ -63,9 +63,9 @@ dev = [ docs = [ "sphinx", - "sphinx-rtd-theme", - "sphinx-autorun", - "sphinxcontrib-jsmath", + "sphinx-rtd-theme", + "sphinx-autorun", + "sphinxcontrib-jsmath", "sphinx-favicon", "sphinx-autodoc-typehints", ] @@ -88,6 +88,7 @@ packages = [ ] [tool.black] +required-version = "23.10.0" line-length = 88 target-version = ['py38'] exclude = "camera_derivatives.py" diff --git a/spatialmath/DualQuaternion.py b/spatialmath/DualQuaternion.py index 3b945d7c..f8ee0f7d 100644 --- a/spatialmath/DualQuaternion.py +++ b/spatialmath/DualQuaternion.py @@ -357,7 +357,6 @@ def SE3(self) -> SE3: if __name__ == "__main__": # pragma: no cover - from spatialmath import SE3, UnitDualQuaternion print(UnitDualQuaternion(SE3())) diff --git a/spatialmath/__init__.py b/spatialmath/__init__.py index 18cb74b4..551481e1 100644 --- a/spatialmath/__init__.py +++ b/spatialmath/__init__.py @@ -17,8 +17,6 @@ from spatialmath.DualQuaternion import DualQuaternion, UnitDualQuaternion from spatialmath.spline import BSplineSE3, InterpSplineSE3, SplineFit -# from spatialmath.Plucker import * -# from spatialmath import base as smb __all__ = [ # pose @@ -46,7 +44,7 @@ "Ellipse", "BSplineSE3", "InterpSplineSE3", - "SplineFit" + "SplineFit", ] try: diff --git a/spatialmath/base/animate.py b/spatialmath/base/animate.py index a2e31f72..1ca8baec 100755 --- a/spatialmath/base/animate.py +++ b/spatialmath/base/animate.py @@ -109,7 +109,9 @@ def __init__( if len(dim) == 2: dim = dim * 3 elif len(dim) != 6: - raise ValueError(f"dim must have 2 or 6 elements, got {dim}. See docstring for details.") + raise ValueError( + f"dim must have 2 or 6 elements, got {dim}. See docstring for details." + ) ax.set_xlim(dim[0:2]) ax.set_ylim(dim[2:4]) ax.set_zlim(dim[4:]) @@ -223,7 +225,7 @@ def update(frame, animation): else: # [unlikely] other types are converted to np array T = np.array(frame) - + if T.shape == (3, 3): T = smb.r2t(T) @@ -606,14 +608,14 @@ def trplot2( smb.trplot2(self.start, ax=self, block=False, **kwargs) def run( - self, + self, movie: Optional[str] = None, axes: Optional[plt.Axes] = None, repeat: bool = False, interval: int = 50, nframes: int = 100, - wait: bool = False, - **kwargs + wait: bool = False, + **kwargs, ): """ Run the animation @@ -663,7 +665,6 @@ def update(frame, animation): animation._draw(T) self.count += 1 # say we're still running - if movie is not None: repeat = False @@ -698,7 +699,9 @@ def update(frame, animation): print("overwriting movie", movie) else: print("creating movie", movie) - FFwriter = animation.FFMpegWriter(fps=1000 / interval, extra_args=["-vcodec", "libx264"]) + FFwriter = animation.FFMpegWriter( + fps=1000 / interval, extra_args=["-vcodec", "libx264"] + ) _ani.save(movie, writer=FFwriter) if wait: @@ -902,8 +905,6 @@ def set_ylabel(self, *args, **kwargs): # plotvol3(2) # tranimate(attitude()) - from spatialmath import base - # T = smb.rpy2r(0.3, 0.4, 0.5) # # smb.tranimate(T, wait=True) # s = smb.tranimate(T, movie=True) diff --git a/spatialmath/base/quaternions.py b/spatialmath/base/quaternions.py index d5652d4a..364a5ea8 100755 --- a/spatialmath/base/quaternions.py +++ b/spatialmath/base/quaternions.py @@ -851,39 +851,49 @@ def qslerp( def _compute_cdf_sin_squared(theta: float): """ Computes the CDF for the distribution of angular magnitude for uniformly sampled rotations. - + :arg theta: angular magnitude :rtype: float :return: cdf of a given angular magnitude :rtype: float - Helper function for uniform sampling of rotations with constrained angular magnitude. + Helper function for uniform sampling of rotations with constrained angular magnitude. This function returns the integral of the pdf of angular magnitudes (2/pi * sin^2(theta/2)). """ return (theta - np.sin(theta)) / np.pi + @lru_cache(maxsize=1) -def _generate_inv_cdf_sin_squared_interp(num_interpolation_points: int = 256) -> interpolate.interp1d: +def _generate_inv_cdf_sin_squared_interp( + num_interpolation_points: int = 256, +) -> interpolate.interp1d: """ Computes an interpolation function for the inverse CDF of the distribution of angular magnitude. - + :arg num_interpolation_points: number of points to use in the interpolation function :rtype: int :return: interpolation function for the inverse cdf of a given angular magnitude :rtype: interpolate.interp1d - Helper function for uniform sampling of rotations with constrained angular magnitude. - This function returns interpolation function for the inverse of the integral of the + Helper function for uniform sampling of rotations with constrained angular magnitude. + This function returns interpolation function for the inverse of the integral of the pdf of angular magnitudes (2/pi * sin^2(theta/2)), which is not analytically defined. """ cdf_sin_squared_interp_angles = np.linspace(0, np.pi, num_interpolation_points) - cdf_sin_squared_interp_values = _compute_cdf_sin_squared(cdf_sin_squared_interp_angles) - return interpolate.interp1d(cdf_sin_squared_interp_values, cdf_sin_squared_interp_angles) + cdf_sin_squared_interp_values = _compute_cdf_sin_squared( + cdf_sin_squared_interp_angles + ) + return interpolate.interp1d( + cdf_sin_squared_interp_values, cdf_sin_squared_interp_angles + ) + -def _compute_inv_cdf_sin_squared(x: ArrayLike, num_interpolation_points: int = 256) -> ArrayLike: +def _compute_inv_cdf_sin_squared( + x: ArrayLike, num_interpolation_points: int = 256 +) -> ArrayLike: """ Computes the inverse CDF of the distribution of angular magnitude. - + :arg x: value for cdf of angular magnitudes :rtype: ArrayLike :arg num_interpolation_points: number of points to use in the interpolation function @@ -891,17 +901,24 @@ def _compute_inv_cdf_sin_squared(x: ArrayLike, num_interpolation_points: int = 2 :return: angular magnitude associate with cdf value :rtype: ArrayLike - Helper function for uniform sampling of rotations with constrained angular magnitude. - This function returns the angle associated with the cdf value derived form integral of + Helper function for uniform sampling of rotations with constrained angular magnitude. + This function returns the angle associated with the cdf value derived form integral of the pdf of angular magnitudes (2/pi * sin^2(theta/2)), which is not analytically defined. """ - inv_cdf_sin_squared_interp = _generate_inv_cdf_sin_squared_interp(num_interpolation_points) + inv_cdf_sin_squared_interp = _generate_inv_cdf_sin_squared_interp( + num_interpolation_points + ) return inv_cdf_sin_squared_interp(x) -def qrand(theta_range:Optional[ArrayLike2] = None, unit: str = "rad", num_interpolation_points: int = 256) -> UnitQuaternionArray: + +def qrand( + theta_range: Optional[ArrayLike2] = None, + unit: str = "rad", + num_interpolation_points: int = 256, +) -> UnitQuaternionArray: """ Random unit-quaternion - + :arg theta_range: angular magnitude range [min,max], defaults to None. :type xrange: 2-element sequence, optional :arg unit: angular units: 'rad' [default], or 'deg' @@ -913,7 +930,7 @@ def qrand(theta_range:Optional[ArrayLike2] = None, unit: str = "rad", num_interp :return: random unit-quaternion :rtype: ndarray(4) - Computes a uniformly distributed random unit-quaternion, with in a maximum + Computes a uniformly distributed random unit-quaternion, with in a maximum angular magnitude, which can be considered equivalent to a random SO(3) rotation. .. runblock:: pycon @@ -924,24 +941,30 @@ def qrand(theta_range:Optional[ArrayLike2] = None, unit: str = "rad", num_interp if theta_range is not None: theta_range = getunit(theta_range, unit) - if(theta_range[0] < 0 or theta_range[1] > np.pi or theta_range[0] > theta_range[1]): - ValueError('Invalid angular range. Must be within the range[0, pi].' - + f' Recieved {theta_range}.') + if ( + theta_range[0] < 0 + or theta_range[1] > np.pi + or theta_range[0] > theta_range[1] + ): + ValueError( + "Invalid angular range. Must be within the range[0, pi]." + + f" Recieved {theta_range}." + ) + + # Sample axis and angle independently, respecting the CDF of the + # angular magnitude under uniform sampling. - # Sample axis and angle independently, respecting the CDF of the - # angular magnitude under uniform sampling. - - # Sample angle using inverse transform sampling based on CDF + # Sample angle using inverse transform sampling based on CDF # of the angular distribution (2/pi * sin^2(theta/2)) theta = _compute_inv_cdf_sin_squared( np.random.uniform( - low=_compute_cdf_sin_squared(theta_range[0]), + low=_compute_cdf_sin_squared(theta_range[0]), high=_compute_cdf_sin_squared(theta_range[1]), ), num_interpolation_points=num_interpolation_points, ) # Sample axis uniformly using 3D normal distributed - v = np.random.randn(3) + v = np.random.randn(3) v /= np.linalg.norm(v) return np.r_[math.cos(theta / 2), (math.sin(theta / 2) * v)] @@ -953,7 +976,7 @@ def qrand(theta_range:Optional[ArrayLike2] = None, unit: str = "rad", num_interp math.sqrt(u[0]) * math.sin(2 * math.pi * u[2]), math.sqrt(u[0]) * math.cos(2 * math.pi * u[2]), ] - + def qmatrix(q: ArrayLike4) -> R4x4: """ diff --git a/spatialmath/base/transforms2d.py b/spatialmath/base/transforms2d.py index 25265fff..ac0696cd 100644 --- a/spatialmath/base/transforms2d.py +++ b/spatialmath/base/transforms2d.py @@ -746,7 +746,7 @@ def trnorm2(T: SE2Array) -> SE2Array: b = unitvec(b) # fmt: off R = np.array([ - [ b[1], b[0]], + [ b[1], b[0]], [-b[0], b[1]] ]) # fmt: on @@ -810,7 +810,7 @@ def tradjoint2(T): (R, t) = smb.tr2rt(cast(SE3Array, T)) # fmt: off return np.block([ - [R, np.c_[t[1], -t[0]].T], + [R, np.c_[t[1], -t[0]].T], [0, 0, 1] ]) # type: ignore # fmt: on @@ -853,12 +853,16 @@ def tr2jac2(T: SE2Array) -> R3x3: @overload -def trinterp2(start: Optional[SO2Array], end: SO2Array, s: float, shortest: bool = True) -> SO2Array: +def trinterp2( + start: Optional[SO2Array], end: SO2Array, s: float, shortest: bool = True +) -> SO2Array: ... @overload -def trinterp2(start: Optional[SE2Array], end: SE2Array, s: float, shortest: bool = True) -> SE2Array: +def trinterp2( + start: Optional[SE2Array], end: SE2Array, s: float, shortest: bool = True +) -> SE2Array: ... diff --git a/spatialmath/base/transforms3d.py b/spatialmath/base/transforms3d.py index 350e1d14..bc2ceb05 100644 --- a/spatialmath/base/transforms3d.py +++ b/spatialmath/base/transforms3d.py @@ -40,7 +40,6 @@ isskew, isskewa, isR, - iseye, tr2rt, Ab2M, ) @@ -1605,12 +1604,16 @@ def trnorm(T: SE3Array) -> SE3Array: @overload -def trinterp(start: Optional[SO3Array], end: SO3Array, s: float, shortest: bool = True) -> SO3Array: +def trinterp( + start: Optional[SO3Array], end: SO3Array, s: float, shortest: bool = True +) -> SO3Array: ... @overload -def trinterp(start: Optional[SE3Array], end: SE3Array, s: float, shortest: bool = True) -> SE3Array: +def trinterp( + start: Optional[SE3Array], end: SE3Array, s: float, shortest: bool = True +) -> SE3Array: ... @@ -1954,15 +1957,15 @@ def rpy2jac(angles: ArrayLike3, order: str = "zyx") -> R3x3: if order == "xyz": # fmt: off - J = np.array([ - [ sp, 0, 1], + J = np.array([ + [ sp, 0, 1], [-cp * sy, cy, 0], [ cp * cy, sy, 0] ]) # type: ignore # fmt: on elif order == "zyx": # fmt: off - J = np.array([ + J = np.array([ [ cp * cy, -sy, 0], [ cp * sy, cy, 0], [-sp, 0, 1], @@ -1970,7 +1973,7 @@ def rpy2jac(angles: ArrayLike3, order: str = "zyx") -> R3x3: # fmt: on elif order == "yxz": # fmt: off - J = np.array([ + J = np.array([ [ cp * sy, cy, 0], [-sp, 0, 1], [ cp * cy, -sy, 0] @@ -2350,7 +2353,7 @@ def rotvelxform( # analytical rates -> angular velocity # fmt: off A = np.array([ - [ S(beta), 0, 1], + [ S(beta), 0, 1], [-S(gamma)*C(beta), C(gamma), 0], # type: ignore [ C(beta)*C(gamma), S(gamma), 0] # type: ignore ]) @@ -2360,7 +2363,7 @@ def rotvelxform( # fmt: off A = np.array([ [0, -S(gamma)/C(beta), C(gamma)/C(beta)], # type: ignore - [0, C(gamma), S(gamma)], + [0, C(gamma), S(gamma)], [1, S(gamma)*T(beta), -C(gamma)*T(beta)] # type: ignore ]) # fmt: on @@ -2724,7 +2727,7 @@ def tr2adjoint(T): (R, t) = tr2rt(T) # fmt: off return np.block([ - [R, skew(t) @ R], + [R, skew(t) @ R], [Z, R] ]) # fmt: on @@ -3432,8 +3435,6 @@ def tranimate(T: Union[SO3Array, SE3Array], **kwargs) -> str: # print(angvelxform([p, q, r], representation='eul')) - import pathlib - # exec( # open( # pathlib.Path(__file__).parent.parent.parent.absolute() diff --git a/spatialmath/baseposelist.py b/spatialmath/baseposelist.py index d729b902..b102b4bb 100644 --- a/spatialmath/baseposelist.py +++ b/spatialmath/baseposelist.py @@ -667,11 +667,12 @@ def unop( else: return [op(x) for x in self.data] + if __name__ == "__main__": from spatialmath import SO3, SO2 - R = SO3([[1,0,0],[0,1,0],[0,0,1]]) + R = SO3([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) print(R.eulervec()) R = SO2([0.3, 0.4, 0.5]) - pass \ No newline at end of file + pass diff --git a/spatialmath/baseposematrix.py b/spatialmath/baseposematrix.py index 1a850600..87071df3 100644 --- a/spatialmath/baseposematrix.py +++ b/spatialmath/baseposematrix.py @@ -377,7 +377,12 @@ def log(self, twist: Optional[bool] = False) -> Union[NDArray, List[NDArray]]: else: return log - def interp(self, end: Optional[bool] = None, s: Union[int, float] = None, shortest: bool = True) -> Self: + def interp( + self, + end: Optional[bool] = None, + s: Union[int, float] = None, + shortest: bool = True, + ) -> Self: """ Interpolate between poses (superclass method) @@ -434,13 +439,19 @@ def interp(self, end: Optional[bool] = None, s: Union[int, float] = None, shorte if self.N == 2: # SO(2) or SE(2) return self.__class__( - [smb.trinterp2(start=self.A, end=end, s=_s, shortest=shortest) for _s in s] + [ + smb.trinterp2(start=self.A, end=end, s=_s, shortest=shortest) + for _s in s + ] ) elif self.N == 3: # SO(3) or SE(3) return self.__class__( - [smb.trinterp(start=self.A, end=end, s=_s, shortest=shortest) for _s in s] + [ + smb.trinterp(start=self.A, end=end, s=_s, shortest=shortest) + for _s in s + ] ) def interp1(self, s: float = None) -> Self: @@ -1692,7 +1703,7 @@ def _op2(left, right: Self, op: Callable): # pylint: disable=no-self-argument if __name__ == "__main__": - from spatialmath import SE3, SE2, SO2 + from spatialmath import SO2 C = SO2(0.5) A = np.array([[10, 0], [0, 1]]) diff --git a/spatialmath/pose3d.py b/spatialmath/pose3d.py index 3b4821e5..9324eda1 100644 --- a/spatialmath/pose3d.py +++ b/spatialmath/pose3d.py @@ -17,7 +17,7 @@ .. inheritance-diagram:: spatialmath.pose3d :top-classes: collections.UserList :parts: 1 - + .. image:: ../figs/pose-values.png """ from __future__ import annotations @@ -35,6 +35,7 @@ from spatialmath.twist import Twist3 from typing import TYPE_CHECKING, Optional + if TYPE_CHECKING: from spatialmath.quaternion import UnitQuaternion @@ -341,7 +342,7 @@ def eulervec(self) -> R3: """ theta, v = smb.tr2angvec(self.R) return theta * v - + # ------------------------------------------------------------------------ # @staticmethod @@ -455,7 +456,9 @@ def Rz(cls, theta, unit: str = "rad") -> Self: return cls([smb.rotz(x, unit=unit) for x in smb.getvector(theta)], check=False) @classmethod - def Rand(cls, N: int = 1, *, theta_range:Optional[ArrayLike2] = None, unit: str = "rad") -> Self: + def Rand( + cls, N: int = 1, *, theta_range: Optional[ArrayLike2] = None, unit: str = "rad" + ) -> Self: """ Construct a new SO(3) from random rotation @@ -481,7 +484,13 @@ def Rand(cls, N: int = 1, *, theta_range:Optional[ArrayLike2] = None, unit: str :seealso: :func:`spatialmath.quaternion.UnitQuaternion.Rand` """ - return cls([smb.q2r(smb.qrand(theta_range=theta_range, unit=unit)) for _ in range(0, N)], check=False) + return cls( + [ + smb.q2r(smb.qrand(theta_range=theta_range, unit=unit)) + for _ in range(0, N) + ], + check=False, + ) @overload @classmethod @@ -1311,11 +1320,11 @@ def yaw_SE2(self, order: str = "zyx") -> SE2: """ if len(self) == 1: if order == "zyx": - return SE2(self.x, self.y, self.rpy(order = order)[2]) + return SE2(self.x, self.y, self.rpy(order=order)[2]) elif order == "xyz": - return SE2(self.z, self.y, self.rpy(order = order)[2]) + return SE2(self.z, self.y, self.rpy(order=order)[2]) elif order == "yxz": - return SE2(self.z, self.x, self.rpy(order = order)[2]) + return SE2(self.z, self.x, self.rpy(order=order)[2]) else: return SE2([e.yaw_SE2() for e in self]) @@ -1601,7 +1610,7 @@ def Rand( xrange: Optional[ArrayLike2] = (-1, 1), yrange: Optional[ArrayLike2] = (-1, 1), zrange: Optional[ArrayLike2] = (-1, 1), - theta_range:Optional[ArrayLike2] = None, + theta_range: Optional[ArrayLike2] = None, unit: str = "rad", ) -> SE3: # pylint: disable=arguments-differ """ @@ -1825,7 +1834,7 @@ def AngleAxis( a rotation of ``θ`` about the vector ``v``. .. math:: - + \mbox{if}\,\, \theta \left\{ \begin{array}{ll} = 0 & \mbox{return identity matrix}\\ \ne 0 & \mbox{v must have a finite length} @@ -2100,11 +2109,7 @@ def Rt( return cls(smb.rt2tr(R, t, check=check), check=check) @classmethod - def CopyFrom( - cls, - T: SE3Array, - check: bool = True - ) -> SE3: + def CopyFrom(cls, T: SE3Array, check: bool = True) -> SE3: """ Create an SE(3) from a 4x4 numpy array that is passed by value. diff --git a/spatialmath/quaternion.py b/spatialmath/quaternion.py index 79bdaf0c..2994b6e6 100644 --- a/spatialmath/quaternion.py +++ b/spatialmath/quaternion.py @@ -17,7 +17,7 @@ from __future__ import annotations import math import numpy as np -from typing import Any, Type +from typing import Any import spatialmath.base as smb from spatialmath.pose3d import SO3, SE3 from spatialmath.baseposelist import BasePoseList @@ -1005,10 +1005,10 @@ def __init__( """ super().__init__() - # handle: UnitQuaternion(v)`` constructs a unit quaternion with specified elements + # handle: UnitQuaternion(v)`` constructs a unit quaternion with specified elements # from ``v`` which is a 4-vector given as a list, tuple, or ndarray(4) if s is None and smb.isvector(v, 4): - v,s = (s,v) + v, s = (s, v) if v is None: # single argument @@ -1248,7 +1248,9 @@ def Rz(cls, angles: ArrayLike, unit: Optional[str] = "rad") -> UnitQuaternion: ) @classmethod - def Rand(cls, N: int = 1, *, theta_range:Optional[ArrayLike2] = None, unit: str = "rad") -> UnitQuaternion: + def Rand( + cls, N: int = 1, *, theta_range: Optional[ArrayLike2] = None, unit: str = "rad" + ) -> UnitQuaternion: """ Construct a new random unit quaternion @@ -1275,7 +1277,10 @@ def Rand(cls, N: int = 1, *, theta_range:Optional[ArrayLike2] = None, unit: str :seealso: :meth:`UnitQuaternion.Rand` """ - return cls([smb.qrand(theta_range=theta_range, unit=unit) for i in range(0, N)], check=False) + return cls( + [smb.qrand(theta_range=theta_range, unit=unit) for i in range(0, N)], + check=False, + ) @classmethod def Eul(cls, *angles: List[float], unit: Optional[str] = "rad") -> UnitQuaternion: diff --git a/spatialmath/spline.py b/spatialmath/spline.py index 8d30bd80..7f849442 100644 --- a/spatialmath/spline.py +++ b/spatialmath/spline.py @@ -2,12 +2,11 @@ # MIT Licence, see details in top-level file: LICENCE """ -Classes for parameterizing a trajectory in SE3 with splines. +Classes for parameterizing a trajectory in SE3 with splines. """ from abc import ABC, abstractmethod -from functools import cached_property -from typing import List, Optional, Tuple, Set +from typing import List, Optional, Tuple import matplotlib.pyplot as plt import numpy as np @@ -160,11 +159,11 @@ def stochastic_downsample_interpolation( epsilon_angle: float = 1e-1, normalize_time: bool = True, bc_type: str = "not-a-knot", - check_type: str = "local" + check_type: str = "local", ) -> Tuple[InterpSplineSE3, List[int]]: """ - Uses a random dropout to downsample a trajectory with an interpolated spline. Keeps the start and - end points of the trajectory. Takes a random order of the remaining indices, and then checks the error bound + Uses a random dropout to downsample a trajectory with an interpolated spline. Keeps the start and + end points of the trajectory. Takes a random order of the remaining indices, and then checks the error bound of just that point if check_type=="local", checks the error of the whole trajectory is check_type=="global". Local is **much** faster. @@ -175,7 +174,7 @@ def stochastic_downsample_interpolation( interpolation_indices = list(range(len(self.pose_data))) - # randomly attempt to remove poses from the trajectory + # randomly attempt to remove poses from the trajectory # always keep the start and end removal_choices = interpolation_indices.copy() removal_choices.remove(0) @@ -197,14 +196,17 @@ def stochastic_downsample_interpolation( SO3(self.spline.spline_so3(sample_time).as_matrix()) ) euclidean_error = np.linalg.norm( - self.pose_data[candidate_removal_index].t - self.spline.spline_xyz(sample_time) + self.pose_data[candidate_removal_index].t + - self.spline.spline_xyz(sample_time) ) elif check_type == "global": angular_error = self.max_angular_error() euclidean_error = self.max_euclidean_error() else: - raise ValueError(f"check_type must be 'local' of 'global', is {check_type}.") - + raise ValueError( + f"check_type must be 'local' of 'global', is {check_type}." + ) + if (angular_error > epsilon_angle) or (euclidean_error > epsilon_xyz): interpolation_indices.append(candidate_removal_index) interpolation_indices.sort() diff --git a/spatialmath/twist.py b/spatialmath/twist.py index f84a0f1b..dcefa840 100644 --- a/spatialmath/twist.py +++ b/spatialmath/twist.py @@ -655,7 +655,7 @@ def RPY(cls, *pos, **kwargs): scalars. Foo bar! - + Example: .. runblock:: pycon diff --git a/tests/base/test_numeric.py b/tests/base/test_numeric.py index e6b9de50..256a3cb1 100755 --- a/tests/base/test_numeric.py +++ b/tests/base/test_numeric.py @@ -8,9 +8,7 @@ """ import numpy as np -import numpy.testing as nt import unittest -import math from spatialmath.base.numeric import * @@ -18,11 +16,9 @@ class TestNumeric(unittest.TestCase): def test_numjac(self): - pass def test_array2str(self): - x = [1.2345678] s = array2str(x) @@ -52,7 +48,6 @@ def test_array2str(self): self.assertEqual(s, "[ 1, 2, 3 | 4, 5, 6 ]") def test_bresenham(self): - x, y = bresenham((-10, -10), (20, 10)) self.assertIsInstance(x, np.ndarray) self.assertEqual(x.ndim, 1) @@ -91,7 +86,6 @@ def test_bresenham(self): self.assertTrue(all(d <= np.sqrt(2))) def test_mpq(self): - data = np.array([[-1, 1, 1, -1], [-1, -1, 1, 1]]) self.assertEqual(mpq_point(data, 0, 0), 4) @@ -99,7 +93,6 @@ def test_mpq(self): self.assertEqual(mpq_point(data, 0, 1), 0) def test_gauss1d(self): - x = np.arange(-10, 10, 0.02) y = gauss1d(2, 1, x) @@ -109,7 +102,6 @@ def test_gauss1d(self): self.assertAlmostEqual(x[m], 2) def test_gauss2d(self): - r = np.arange(-10, 10, 0.02) X, Y = np.meshgrid(r, r) Z = gauss2d([2, 3], np.eye(2), X, Y) @@ -121,5 +113,4 @@ def test_gauss2d(self): # ---------------------------------------------------------------------------------------# if __name__ == "__main__": - unittest.main() diff --git a/tests/base/test_symbolic.py b/tests/base/test_symbolic.py index 7a503b5b..cc441cc5 100644 --- a/tests/base/test_symbolic.py +++ b/tests/base/test_symbolic.py @@ -46,7 +46,6 @@ def test_issymbol(self): @unittest.skipUnless(_symbolics, "sympy required") def test_functions(self): - theta = symbol("theta") self.assertTrue(isinstance(sin(theta), sp.Expr)) self.assertTrue(isinstance(sin(1.0), float)) @@ -57,12 +56,11 @@ def test_functions(self): self.assertTrue(isinstance(sqrt(theta), sp.Expr)) self.assertTrue(isinstance(sqrt(1.0), float)) - x = (theta - 1) * (theta + 1) - theta ** 2 + x = (theta - 1) * (theta + 1) - theta**2 self.assertTrue(math.isclose(simplify(x).evalf(), -1)) @unittest.skipUnless(_symbolics, "sympy required") def test_constants(self): - x = zero() self.assertTrue(isinstance(x, sp.Expr)) self.assertTrue(math.isclose(x.evalf(), 0)) @@ -82,5 +80,4 @@ def test_constants(self): # ---------------------------------------------------------------------------------------# if __name__ == "__main__": # pragma: no cover - unittest.main() diff --git a/tests/base/test_transforms.py b/tests/base/test_transforms.py index 71b01bb3..67f3e776 100755 --- a/tests/base/test_transforms.py +++ b/tests/base/test_transforms.py @@ -12,13 +12,9 @@ import numpy.testing as nt import unittest from math import pi -import math from scipy.linalg import logm, expm from spatialmath.base import * -from spatialmath.base import sym - -import matplotlib.pyplot as plt class TestLie(unittest.TestCase): @@ -49,7 +45,6 @@ def test_skew(self): ) # check contents, vex already verified def test_vexa(self): - S = np.array([[0, -3, 1], [3, 0, 2], [0, 0, 0]]) nt.assert_array_almost_equal(vexa(S), np.array([1, 2, 3])) @@ -80,7 +75,6 @@ def test_skewa(self): ) # check contents, vexa already verified def test_trlog(self): - # %%% SO(3) tests # zero rotation case nt.assert_array_almost_equal(trlog(np.eye(3)), skew([0, 0, 0])) @@ -189,7 +183,6 @@ def test_trlog(self): # TODO def test_trexp(self): - # %% SO(3) tests # % so(3) @@ -271,7 +264,6 @@ def test_trexp(self): nt.assert_array_almost_equal(trexp(trlog(T)), T) def test_trexp2(self): - # % so(2) # zero rotation case @@ -323,5 +315,4 @@ def test_trnorm(self): # ---------------------------------------------------------------------------------------# if __name__ == "__main__": - unittest.main() diff --git a/tests/base/test_transforms2d.py b/tests/base/test_transforms2d.py index ff099930..f78e38e3 100755 --- a/tests/base/test_transforms2d.py +++ b/tests/base/test_transforms2d.py @@ -12,7 +12,7 @@ import unittest from math import pi import math -from scipy.linalg import logm, expm +from scipy.linalg import logm import pytest import sys @@ -27,7 +27,6 @@ skewa, homtrans, ) -from spatialmath.base.numeric import numjac import matplotlib.pyplot as plt @@ -125,22 +124,14 @@ def test_pos2tr2(self): nt.assert_array_almost_equal( transl2([1, 2]), np.array([[1, 0, 1], [0, 1, 2], [0, 0, 1]]) ) - nt.assert_array_almost_equal( - tr2pos2(pos2tr2(1, 2)), np.array([1, 2]) - ) + nt.assert_array_almost_equal(tr2pos2(pos2tr2(1, 2)), np.array([1, 2])) def test_tr2jac2(self): T = trot2(0.3, t=[4, 5]) jac2 = tr2jac2(T) - nt.assert_array_almost_equal( - jac2[:2, :2], smb.t2r(T) - ) - nt.assert_array_almost_equal( - jac2[:3, 2], np.array([0, 0, 1]) - ) - nt.assert_array_almost_equal( - jac2[2, :3], np.array([0, 0, 1]) - ) + nt.assert_array_almost_equal(jac2[:2, :2], smb.t2r(T)) + nt.assert_array_almost_equal(jac2[:3, 2], np.array([0, 0, 1])) + nt.assert_array_almost_equal(jac2[2, :3], np.array([0, 0, 1])) def test_xyt2tr(self): T = xyt2tr([1, 2, 0]) diff --git a/tests/base/test_transforms3d.py b/tests/base/test_transforms3d.py index 2f1e6049..8b2fb080 100755 --- a/tests/base/test_transforms3d.py +++ b/tests/base/test_transforms3d.py @@ -12,8 +12,7 @@ import numpy.testing as nt import unittest from math import pi -import math -from scipy.linalg import logm, expm +from scipy.linalg import logm from spatialmath.base.transforms3d import * from spatialmath.base.transformsNd import isR, t2r, r2t, rt2tr, skew @@ -518,7 +517,7 @@ def test_tr2angvec(self): nt.assert_array_almost_equal(v, np.r_[0, 1, 0]) true_ang = 1.51 - true_vec = np.array([0., 1., 0.]) + true_vec = np.array([0.0, 1.0, 0.0]) eps = 1e-08 # show that tr2angvec works on true rotation matrix @@ -806,6 +805,7 @@ def test_x2tr(self): x2tr(x, representation="exp"), transl(t) @ r2t(trexp(gamma)) ) + # ---------------------------------------------------------------------------------------# if __name__ == "__main__": unittest.main() diff --git a/tests/base/test_transformsNd.py b/tests/base/test_transformsNd.py index 92d9e2a3..14c7fd42 100755 --- a/tests/base/test_transformsNd.py +++ b/tests/base/test_transformsNd.py @@ -11,8 +11,6 @@ import numpy.testing as nt import unittest from math import pi -import math -from scipy.linalg import logm, expm from spatialmath.base.transformsNd import * from spatialmath.base.transforms3d import trotx, transl, rotx, isrot, ishom @@ -25,7 +23,6 @@ from spatialmath.base.symbolic import symbol except ImportError: _symbolics = False -import matplotlib.pyplot as plt class TestND(unittest.TestCase): @@ -58,7 +55,7 @@ def test_r2t(self): with self.assertRaises(ValueError): r2t(np.eye(3, 4)) - + _ = r2t(np.ones((3, 3)), check=False) with self.assertRaises(ValueError): r2t(np.ones((3, 3)), check=True) diff --git a/tests/base/test_vectors.py b/tests/base/test_vectors.py index 592c2d16..15c6a451 100755 --- a/tests/base/test_vectors.py +++ b/tests/base/test_vectors.py @@ -12,7 +12,6 @@ import unittest from math import pi import math -from scipy.linalg import logm, expm from spatialmath.base.vectors import * @@ -218,18 +217,10 @@ def test_unittwist_norm(self): self.assertIsNone(a[1]) def test_unittwist2(self): - nt.assert_array_almost_equal( - unittwist2([1, 0, 0]), np.r_[1, 0, 0] - ) - nt.assert_array_almost_equal( - unittwist2([0, 2, 0]), np.r_[0, 1, 0] - ) - nt.assert_array_almost_equal( - unittwist2([0, 0, -3]), np.r_[0, 0, -1] - ) - nt.assert_array_almost_equal( - unittwist2([2, 0, -2]), np.r_[1, 0, -1] - ) + nt.assert_array_almost_equal(unittwist2([1, 0, 0]), np.r_[1, 0, 0]) + nt.assert_array_almost_equal(unittwist2([0, 2, 0]), np.r_[0, 1, 0]) + nt.assert_array_almost_equal(unittwist2([0, 0, -3]), np.r_[0, 0, -1]) + nt.assert_array_almost_equal(unittwist2([2, 0, -2]), np.r_[1, 0, -1]) self.assertIsNone(unittwist2([0, 0, 0])) @@ -329,14 +320,30 @@ def test_wrap(self): theta = angle_factor * pi self.assertAlmostEqual(angle_wrap(theta), wrap_mpi_pi(theta)) self.assertAlmostEqual(angle_wrap(-theta), wrap_mpi_pi(-theta)) - self.assertAlmostEqual(angle_wrap(theta=theta, mode="-pi:pi"), wrap_mpi_pi(theta)) - self.assertAlmostEqual(angle_wrap(theta=-theta, mode="-pi:pi"), wrap_mpi_pi(-theta)) - self.assertAlmostEqual(angle_wrap(theta=theta, mode="0:2pi"), wrap_0_2pi(theta)) - self.assertAlmostEqual(angle_wrap(theta=-theta, mode="0:2pi"), wrap_0_2pi(-theta)) - self.assertAlmostEqual(angle_wrap(theta=theta, mode="0:pi"), wrap_0_pi(theta)) - self.assertAlmostEqual(angle_wrap(theta=-theta, mode="0:pi"), wrap_0_pi(-theta)) - self.assertAlmostEqual(angle_wrap(theta=theta, mode="-pi/2:pi/2"), wrap_mpi2_pi2(theta)) - self.assertAlmostEqual(angle_wrap(theta=-theta, mode="-pi/2:pi/2"), wrap_mpi2_pi2(-theta)) + self.assertAlmostEqual( + angle_wrap(theta=theta, mode="-pi:pi"), wrap_mpi_pi(theta) + ) + self.assertAlmostEqual( + angle_wrap(theta=-theta, mode="-pi:pi"), wrap_mpi_pi(-theta) + ) + self.assertAlmostEqual( + angle_wrap(theta=theta, mode="0:2pi"), wrap_0_2pi(theta) + ) + self.assertAlmostEqual( + angle_wrap(theta=-theta, mode="0:2pi"), wrap_0_2pi(-theta) + ) + self.assertAlmostEqual( + angle_wrap(theta=theta, mode="0:pi"), wrap_0_pi(theta) + ) + self.assertAlmostEqual( + angle_wrap(theta=-theta, mode="0:pi"), wrap_0_pi(-theta) + ) + self.assertAlmostEqual( + angle_wrap(theta=theta, mode="-pi/2:pi/2"), wrap_mpi2_pi2(theta) + ) + self.assertAlmostEqual( + angle_wrap(theta=-theta, mode="-pi/2:pi/2"), wrap_mpi2_pi2(-theta) + ) with self.assertRaises(ValueError): angle_wrap(theta=theta, mode="foo") diff --git a/tests/test_baseposelist.py b/tests/test_baseposelist.py index 30da5681..c3f9b311 100644 --- a/tests/test_baseposelist.py +++ b/tests/test_baseposelist.py @@ -2,28 +2,28 @@ import numpy as np from spatialmath.baseposelist import BasePoseList + # create a subclass to test with, its value is a scalar class X(BasePoseList): def __init__(self, value=0, check=False): super().__init__() self.data = [value] - + @staticmethod def _identity(): return 0 @property def shape(self): - return (1,1) + return (1, 1) @staticmethod def isvalid(x): return True -class TestBasePoseList(unittest.TestCase): +class TestBasePoseList(unittest.TestCase): def test_constructor(self): - x = X() self.assertIsInstance(x, X) self.assertEqual(len(x), 1) @@ -43,13 +43,13 @@ def test_setget(self): for i in range(0, 10): x[i] = X(2 * i) - for i,v in enumerate(x): + for i, v in enumerate(x): self.assertEqual(v.A, 2 * i) def test_append(self): x = X.Empty() for i in range(0, 10): - x.append(X(i+1)) + x.append(X(i + 1)) self.assertEqual(len(x), 10) self.assertEqual([xx.A for xx in x], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) @@ -63,7 +63,7 @@ def test_extend(self): x.extend(y) self.assertEqual(len(x), 10) self.assertEqual([xx.A for xx in x], [1, 2, 3, 4, 5, 10, 11, 12, 13, 14]) - + def test_insert(self): x = X.Alloc(10) for i in range(0, 10): @@ -134,13 +134,13 @@ def test_unop(self): self.assertEqual(x.unop(f), [2, 4, 6, 8, 10]) y = x.unop(f, matrix=True) - self.assertEqual(y.shape, (5,1)) + self.assertEqual(y.shape, (5, 1)) self.assertTrue(np.all(y - np.c_[2, 4, 6, 8, 10].T == 0)) def test_arghandler(self): pass + # ---------------------------------------------------------------------------------------# -if __name__ == '__main__': - - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_dualquaternion.py b/tests/test_dualquaternion.py index 39c5fc03..ed785313 100644 --- a/tests/test_dualquaternion.py +++ b/tests/test_dualquaternion.py @@ -1,4 +1,3 @@ -import math from math import pi import numpy as np @@ -6,7 +5,6 @@ import unittest from spatialmath import DualQuaternion, UnitDualQuaternion, Quaternion, SE3 -from spatialmath import base def qcompare(x, y): @@ -20,32 +18,29 @@ def qcompare(x, y): y = y.A nt.assert_array_almost_equal(x, y) -class TestDualQuaternion(unittest.TestCase): +class TestDualQuaternion(unittest.TestCase): def test_init(self): + dq = DualQuaternion(Quaternion([1.0, 2, 3, 4]), Quaternion([5.0, 6, 7, 8])) + nt.assert_array_almost_equal(dq.vec, np.r_[1, 2, 3, 4, 5, 6, 7, 8]) - dq = DualQuaternion(Quaternion([1.,2,3,4]), Quaternion([5.,6,7,8])) - nt.assert_array_almost_equal(dq.vec, np.r_[1,2,3,4,5,6,7,8]) - - dq = DualQuaternion([1.,2,3,4,5,6,7,8]) - nt.assert_array_almost_equal(dq.vec, np.r_[1,2,3,4,5,6,7,8]) - dq = DualQuaternion(np.r_[1,2,3,4,5,6,7,8]) - nt.assert_array_almost_equal(dq.vec, np.r_[1,2,3,4,5,6,7,8]) + dq = DualQuaternion([1.0, 2, 3, 4, 5, 6, 7, 8]) + nt.assert_array_almost_equal(dq.vec, np.r_[1, 2, 3, 4, 5, 6, 7, 8]) + dq = DualQuaternion(np.r_[1, 2, 3, 4, 5, 6, 7, 8]) + nt.assert_array_almost_equal(dq.vec, np.r_[1, 2, 3, 4, 5, 6, 7, 8]) def test_pure(self): - - dq = DualQuaternion.Pure([1.,2,3]) - nt.assert_array_almost_equal(dq.vec, np.r_[1,0,0,0, 0,1,2,3]) + dq = DualQuaternion.Pure([1.0, 2, 3]) + nt.assert_array_almost_equal(dq.vec, np.r_[1, 0, 0, 0, 0, 1, 2, 3]) def test_strings(self): - - dq = DualQuaternion(Quaternion([1.,2,3,4]), Quaternion([5.,6,7,8])) + dq = DualQuaternion(Quaternion([1.0, 2, 3, 4]), Quaternion([5.0, 6, 7, 8])) self.assertIsInstance(str(dq), str) self.assertIsInstance(repr(dq), str) def test_conj(self): - dq = DualQuaternion(Quaternion([1.,2,3,4]), Quaternion([5.,6,7,8])) - nt.assert_array_almost_equal(dq.conj().vec, np.r_[1,-2,-3,-4, 5,-6,-7,-8]) + dq = DualQuaternion(Quaternion([1.0, 2, 3, 4]), Quaternion([5.0, 6, 7, 8])) + nt.assert_array_almost_equal(dq.conj().vec, np.r_[1, -2, -3, -4, 5, -6, -7, -8]) # def test_norm(self): # q1 = Quaternion([1.,2,3,4]) @@ -55,26 +50,25 @@ def test_conj(self): # nt.assert_array_almost_equal(dq.norm(), (q1.norm(), q2.norm())) def test_plus(self): - dq = DualQuaternion(Quaternion([1.,2,3,4]), Quaternion([5.,6,7,8])) + dq = DualQuaternion(Quaternion([1.0, 2, 3, 4]), Quaternion([5.0, 6, 7, 8])) s = dq + dq - nt.assert_array_almost_equal(s.vec, 2*np.r_[1,2,3,4,5,6,7,8]) + nt.assert_array_almost_equal(s.vec, 2 * np.r_[1, 2, 3, 4, 5, 6, 7, 8]) def test_minus(self): - dq = DualQuaternion(Quaternion([1.,2,3,4]), Quaternion([5.,6,7,8])) + dq = DualQuaternion(Quaternion([1.0, 2, 3, 4]), Quaternion([5.0, 6, 7, 8])) s = dq - dq nt.assert_array_almost_equal(s.vec, np.zeros((8,))) def test_matrix(self): - - dq1 = DualQuaternion(Quaternion([1.,2,3,4]), Quaternion([5.,6,7,8])) + dq1 = DualQuaternion(Quaternion([1.0, 2, 3, 4]), Quaternion([5.0, 6, 7, 8])) M = dq1.matrix() self.assertIsInstance(M, np.ndarray) - self.assertEqual(M.shape, (8,8)) + self.assertEqual(M.shape, (8, 8)) def test_multiply(self): - dq1 = DualQuaternion(Quaternion([1.,2,3,4]), Quaternion([5.,6,7,8])) - dq2 = DualQuaternion(Quaternion([4,3,2,1]), Quaternion([5,6,7,8])) + dq1 = DualQuaternion(Quaternion([1.0, 2, 3, 4]), Quaternion([5.0, 6, 7, 8])) + dq2 = DualQuaternion(Quaternion([4, 3, 2, 1]), Quaternion([5, 6, 7, 8])) M = dq1.matrix() v = dq2.vec @@ -85,21 +79,19 @@ def test_unit(self): class TestUnitDualQuaternion(unittest.TestCase): - def test_init(self): - - T = SE3.Rx(pi/4) + T = SE3.Rx(pi / 4) dq = UnitDualQuaternion(T) nt.assert_array_almost_equal(dq.SE3().A, T.A) def test_norm(self): - T = SE3.Rx(pi/4) + T = SE3.Rx(pi / 4) dq = UnitDualQuaternion(T) - nt.assert_array_almost_equal(dq.norm(), (1,0)) + nt.assert_array_almost_equal(dq.norm(), (1, 0)) def test_multiply(self): - T1 = SE3.Rx(pi/4) - T2 = SE3.Rz(-pi/3) + T1 = SE3.Rx(pi / 4) + T2 = SE3.Rz(-pi / 3) T = T1 * T2 @@ -111,6 +103,5 @@ def test_multiply(self): # ---------------------------------------------------------------------------------------# -if __name__ == '__main__': # pragma: no cover - +if __name__ == "__main__": # pragma: no cover unittest.main() diff --git a/tests/test_pose3d.py b/tests/test_pose3d.py index 58396441..70b33ce0 100755 --- a/tests/test_pose3d.py +++ b/tests/test_pose3d.py @@ -242,8 +242,8 @@ def test_constructor_TwoVec(self): nt.assert_almost_equal(R.R[:, 0], v3 / np.linalg.norm(v3), 5) def test_conversion(self): - R = SO3.AngleAxis(0.7, [1,2,3]) - q = UnitQuaternion([11,7,3,-6]) + R = SO3.AngleAxis(0.7, [1, 2, 3]) + q = UnitQuaternion([11, 7, 3, -6]) R_from_q = SO3(q.R) q_from_R = UnitQuaternion(R) @@ -716,7 +716,6 @@ def test_functions_lie(self): nt.assert_equal(R, SO3.EulerVec(R.eulervec())) np.testing.assert_equal((R.inv() * R).eulervec(), np.zeros(3)) - def test_rotatedvector(self): v1 = [1, 2, 3] R = SO3.Eul(0.3, 0.4, 0.5) @@ -759,7 +758,6 @@ def test_mean(self): array_compare(m, SO3.RPY(0.1, 0.2, 0.3)) - # ============================== SE3 =====================================# @@ -872,7 +870,7 @@ def test_constructor(self): nt.assert_equal(len(R), 1) self.assertIsInstance(R, SE3) - # random + # random T = SE3.Rand() R = T.R t = T.t diff --git a/tests/test_quaternion.py b/tests/test_quaternion.py index 403b3c8d..75d31b7c 100644 --- a/tests/test_quaternion.py +++ b/tests/test_quaternion.py @@ -48,20 +48,18 @@ def test_constructor_variants(self): nt.assert_array_almost_equal( UnitQuaternion.Rz(-90, "deg").vec, np.r_[1, 0, 0, -1] / math.sqrt(2) ) - + np.random.seed(73) q = UnitQuaternion.Rand(theta_range=(0.1, 0.7)) self.assertIsInstance(q, UnitQuaternion) self.assertLessEqual(q.angvec()[0], 0.7) self.assertGreaterEqual(q.angvec()[0], 0.1) - q = UnitQuaternion.Rand(theta_range=(0.1, 0.7)) self.assertIsInstance(q, UnitQuaternion) self.assertLessEqual(q.angvec()[0], 0.7) self.assertGreaterEqual(q.angvec()[0], 0.1) - def test_constructor(self): qcompare(UnitQuaternion(), [1, 0, 0, 0]) @@ -83,8 +81,8 @@ def test_constructor(self): qcompare(UnitQuaternion(2, [0, 0, 0]), np.r_[1, 0, 0, 0]) qcompare(UnitQuaternion(-2, [0, 0, 0]), np.r_[1, 0, 0, 0]) - qcompare(UnitQuaternion([1, 2, 3, 4]), UnitQuaternion(v = [1, 2, 3, 4])) - qcompare(UnitQuaternion(s = 1, v = [2, 3, 4]), UnitQuaternion(v = [1, 2, 3, 4])) + qcompare(UnitQuaternion([1, 2, 3, 4]), UnitQuaternion(v=[1, 2, 3, 4])) + qcompare(UnitQuaternion(s=1, v=[2, 3, 4]), UnitQuaternion(v=[1, 2, 3, 4])) # from R @@ -824,8 +822,8 @@ def test_constructor(self): nt.assert_array_almost_equal(Quaternion(2, [0, 0, 0]).vec, [2, 0, 0, 0]) nt.assert_array_almost_equal(Quaternion(-2, [0, 0, 0]).vec, [-2, 0, 0, 0]) - qcompare(Quaternion([1, 2, 3, 4]), Quaternion(v = [1, 2, 3, 4])) - qcompare(Quaternion(s = 1, v = [2, 3, 4]), Quaternion(v = [1, 2, 3, 4])) + qcompare(Quaternion([1, 2, 3, 4]), Quaternion(v=[1, 2, 3, 4])) + qcompare(Quaternion(s=1, v=[2, 3, 4]), Quaternion(v=[1, 2, 3, 4])) # pure v = [5, 6, 7] diff --git a/tests/test_spline.py b/tests/test_spline.py index 361bc28f..9f27c608 100644 --- a/tests/test_spline.py +++ b/tests/test_spline.py @@ -27,7 +27,10 @@ def test_evaluation(self): def test_visualize(self): spline = BSplineSE3(self.control_poses) - spline.visualize(sample_times= np.linspace(0, 1.0, 100), animate=True, repeat=False) + spline.visualize( + sample_times=np.linspace(0, 1.0, 100), animate=True, repeat=False + ) + class TestInterpSplineSE3: waypoints = [ @@ -56,14 +59,20 @@ def test_evaluation(self): for time, pose in zip(norm_time, self.waypoints): nt.assert_almost_equal(spline(time).angdist(pose), 0.0) nt.assert_almost_equal(np.linalg.norm(spline(time).t - pose.t), 0.0) - + def test_small_delta_t(self): - InterpSplineSE3(np.linspace(0, InterpSplineSE3._e, len(self.waypoints)), self.waypoints) + InterpSplineSE3( + np.linspace(0, InterpSplineSE3._e, len(self.waypoints)), self.waypoints + ) def test_visualize(self): spline = InterpSplineSE3(self.times, self.waypoints) - spline.visualize(sample_times= np.linspace(0, self.time_horizon, 100), animate=True, repeat=False) - + spline.visualize( + sample_times=np.linspace(0, self.time_horizon, 100), + animate=True, + repeat=False, + ) + class TestSplineFit: num_data_points = 300 @@ -74,7 +83,11 @@ class TestSplineFit: timestamps = np.linspace(0, 1, num_data_points) trajectory = [ SE3.Rt( - t=[t * 0.4, 0.4 * np.sin(t * 2 * np.pi * 0.5), 0.4 * np.cos(t * 2 * np.pi * 0.5)], + t=[ + t * 0.4, + 0.4 * np.sin(t * 2 * np.pi * 0.5), + 0.4 * np.cos(t * 2 * np.pi * 0.5), + ], R=SO3.Rx(t * 2 * np.pi * 0.5), ) for t in timestamps * time_horizon @@ -85,11 +98,15 @@ def test_spline_fit(self): spline, kept_indices = fit.stochastic_downsample_interpolation() fraction_points_removed = 1.0 - len(kept_indices) / self.num_data_points - - assert(fraction_points_removed > 0.2) - assert(len(spline.control_poses)==len(kept_indices)) - assert(len(spline.timepoints)==len(kept_indices)) - - assert( fit.max_angular_error() < np.deg2rad(5.0) ) - assert( fit.max_angular_error() < 0.1 ) - spline.visualize(sample_times= np.linspace(0, self.time_horizon, 100), animate=True, repeat=False) \ No newline at end of file + + assert fraction_points_removed > 0.2 + assert len(spline.control_poses) == len(kept_indices) + assert len(spline.timepoints) == len(kept_indices) + + assert fit.max_angular_error() < np.deg2rad(5.0) + assert fit.max_angular_error() < 0.1 + spline.visualize( + sample_times=np.linspace(0, self.time_horizon, 100), + animate=True, + repeat=False, + ) diff --git a/tests/test_twist.py b/tests/test_twist.py index 12660c7d..70f237a8 100755 --- a/tests/test_twist.py +++ b/tests/test_twist.py @@ -1,5 +1,4 @@ import numpy.testing as nt -import matplotlib.pyplot as plt import unittest """ @@ -7,12 +6,14 @@ """ from math import pi from spatialmath.twist import * + # from spatialmath import super_pose # as sp from spatialmath.base import * from spatialmath.baseposematrix import BasePoseMatrix from spatialmath import SE2, SE3 from spatialmath.twist import BaseTwist + def array_compare(x, y): if isinstance(x, BasePoseMatrix): x = x.A @@ -26,9 +27,7 @@ def array_compare(x, y): class Twist3dTest(unittest.TestCase): - def test_constructor(self): - s = [1, 2, 3, 4, 5, 6] x = Twist3(s) self.assertIsInstance(x, Twist3) @@ -36,29 +35,28 @@ def test_constructor(self): array_compare(x.v, [1, 2, 3]) array_compare(x.w, [4, 5, 6]) array_compare(x.S, s) - - x = Twist3([1,2,3], [4,5,6]) + + x = Twist3([1, 2, 3], [4, 5, 6]) array_compare(x.v, [1, 2, 3]) array_compare(x.w, [4, 5, 6]) array_compare(x.S, s) y = Twist3(x) array_compare(x, y) - + x = Twist3(SE3()) - array_compare(x, [0,0,0,0,0,0]) - - + array_compare(x, [0, 0, 0, 0, 0, 0]) + def test_list(self): x = Twist3([1, 0, 0, 0, 0, 0]) y = Twist3([1, 0, 0, 0, 0, 0]) - + a = Twist3(x) a.append(y) self.assertEqual(len(a), 2) array_compare(a[0], x) array_compare(a[1], y) - + def test_conversion_SE3(self): T = SE3.Rx(0) tw = Twist3(T) @@ -68,134 +66,145 @@ def test_conversion_SE3(self): T = SE3.Rx(0) * SE3(1, 2, 3) array_compare(Twist3(T).SE3(), T) - + def test_conversion_se3(self): s = [1, 2, 3, 4, 5, 6] x = Twist3(s) - - array_compare(x.skewa(), np.array([[ 0., -6., 5., 1.], - [ 6., 0., -4., 2.], - [-5., 4., 0., 3.], - [ 0., 0., 0., 0.]])) - + + array_compare( + x.skewa(), + np.array( + [ + [0.0, -6.0, 5.0, 1.0], + [6.0, 0.0, -4.0, 2.0], + [-5.0, 4.0, 0.0, 3.0], + [0.0, 0.0, 0.0, 0.0], + ] + ), + ) + def test_conversion_Plucker(self): pass - + def test_list_constuctor(self): x = Twist3([1, 0, 0, 0, 0, 0]) - - a = Twist3([x,x,x,x]) + + a = Twist3([x, x, x, x]) self.assertIsInstance(a, Twist3) self.assertEqual(len(a), 4) - + a = Twist3([x.skewa(), x.skewa(), x.skewa(), x.skewa()]) self.assertIsInstance(a, Twist3) self.assertEqual(len(a), 4) - + a = Twist3([x.S, x.S, x.S, x.S]) self.assertIsInstance(a, Twist3) self.assertEqual(len(a), 4) - + s = np.r_[1, 2, 3, 4, 5, 6] a = Twist3([s, s, s, s]) self.assertIsInstance(a, Twist3) self.assertEqual(len(a), 4) - + def test_predicate(self): x = Twist3.UnitRevolute([1, 2, 3], [0, 0, 0]) self.assertFalse(x.isprismatic) - + # check prismatic twist x = Twist3.UnitPrismatic([1, 2, 3]) self.assertTrue(x.isprismatic) - + self.assertTrue(Twist3.isvalid(x.skewa())) self.assertTrue(Twist3.isvalid(x.S)) - + self.assertFalse(Twist3.isvalid(2)) self.assertFalse(Twist3.isvalid(np.eye(4))) - + def test_str(self): x = Twist3([1, 2, 3, 4, 5, 6]) s = str(x) self.assertIsInstance(s, str) self.assertEqual(len(s), 14) - self.assertEqual(s.count('\n'), 0) - + self.assertEqual(s.count("\n"), 0) + x.append(x) s = str(x) self.assertIsInstance(s, str) self.assertEqual(len(s), 29) - self.assertEqual(s.count('\n'), 1) - + self.assertEqual(s.count("\n"), 1) + def test_variant_constructors(self): - # check rotational twist x = Twist3.UnitRevolute([1, 2, 3], [0, 0, 0]) array_compare(x, np.r_[0, 0, 0, unitvec([1, 2, 3])]) - + # check prismatic twist x = Twist3.UnitPrismatic([1, 2, 3]) - array_compare(x, np.r_[unitvec([1, 2, 3]), 0, 0, 0, ]) - + array_compare( + x, + np.r_[ + unitvec([1, 2, 3]), + 0, + 0, + 0, + ], + ) + def test_SE3_twists(self): - tw = Twist3( SE3.Rx(0) ) - array_compare(tw, np.r_[0, 0, 0, 0, 0, 0]) - - tw = Twist3( SE3.Rx(pi / 2) ) - array_compare(tw, np.r_[0, 0, 0, pi / 2, 0, 0]) - - tw = Twist3( SE3.Ry(pi / 2) ) - array_compare(tw, np.r_[0, 0, 0, 0, pi / 2, 0]) - - tw = Twist3( SE3.Rz(pi / 2) ) - array_compare(tw, np.r_[0, 0, 0, 0, 0, pi / 2]) - - tw = Twist3( SE3([1, 2, 3]) ) - array_compare(tw, [1, 2, 3, 0, 0, 0]) - - tw = Twist3( SE3([1, 2, 3]) * SE3.Ry(pi / 2)) - array_compare(tw, np.r_[-pi / 2, 2, pi, 0, pi / 2, 0]) - + tw = Twist3(SE3.Rx(0)) + array_compare(tw, np.r_[0, 0, 0, 0, 0, 0]) + + tw = Twist3(SE3.Rx(pi / 2)) + array_compare(tw, np.r_[0, 0, 0, pi / 2, 0, 0]) + + tw = Twist3(SE3.Ry(pi / 2)) + array_compare(tw, np.r_[0, 0, 0, 0, pi / 2, 0]) + + tw = Twist3(SE3.Rz(pi / 2)) + array_compare(tw, np.r_[0, 0, 0, 0, 0, pi / 2]) + + tw = Twist3(SE3([1, 2, 3])) + array_compare(tw, [1, 2, 3, 0, 0, 0]) + + tw = Twist3(SE3([1, 2, 3]) * SE3.Ry(pi / 2)) + array_compare(tw, np.r_[-pi / 2, 2, pi, 0, pi / 2, 0]) + def test_exp(self): tw = Twist3.UnitRevolute([1, 0, 0], [0, 0, 0]) - array_compare(tw.exp(pi/2), SE3.Rx(pi/2)) - + array_compare(tw.exp(pi / 2), SE3.Rx(pi / 2)) + tw = Twist3.UnitRevolute([0, 1, 0], [0, 0, 0]) - array_compare(tw.exp(pi/2), SE3.Ry(pi/2)) - + array_compare(tw.exp(pi / 2), SE3.Ry(pi / 2)) + tw = Twist3.UnitRevolute([0, 0, 1], [0, 0, 0]) - array_compare(tw.exp(pi/2), SE3.Rz(pi / 2)) - + array_compare(tw.exp(pi / 2), SE3.Rz(pi / 2)) + def test_arith(self): - # check overloaded * T1 = SE3(1, 2, 3) * SE3.Rx(pi / 2) T2 = SE3(4, 5, -6) * SE3.Ry(-pi / 2) - + x1 = Twist3(T1) x2 = Twist3(T2) - array_compare( (x1 * x2).exp(), T1 * T2) - array_compare( (x2 * x1).exp(), T2 * T1) - + array_compare((x1 * x2).exp(), T1 * T2) + array_compare((x2 * x1).exp(), T2 * T1) + def test_prod(self): # check prod T1 = SE3(1, 2, 3) * SE3.Rx(pi / 2) T2 = SE3(4, 5, -6) * SE3.Ry(-pi / 2) - + x1 = Twist3(T1) x2 = Twist3(T2) - + x = Twist3([x1, x2]) - array_compare( x.prod().SE3(), T1 * T2) - + array_compare(x.prod().SE3(), T1 * T2) + class Twist2dTest(unittest.TestCase): - def test_constructor(self): - s = [1, 2, 3] x = Twist2(s) self.assertIsInstance(x, Twist2) @@ -203,33 +212,32 @@ def test_constructor(self): array_compare(x.v, [1, 2]) array_compare(x.w, [3]) array_compare(x.S, s) - - x = Twist2([1,2], 3) + + x = Twist2([1, 2], 3) array_compare(x.v, [1, 2]) array_compare(x.w, [3]) array_compare(x.S, s) y = Twist2(x) array_compare(x, y) - + # construct from SE2 x = Twist2(SE2()) - array_compare(x, [0,0,0]) - - x = Twist2( SE2(0, 0, pi / 2)) + array_compare(x, [0, 0, 0]) + + x = Twist2(SE2(0, 0, pi / 2)) array_compare(x, np.r_[0, 0, pi / 2]) - - x = Twist2( SE2(1, 2,0 )) + + x = Twist2(SE2(1, 2, 0)) array_compare(x, np.r_[1, 2, 0]) - - x = Twist2( SE2(1, 2, pi / 2)) + + x = Twist2(SE2(1, 2, pi / 2)) array_compare(x, np.r_[3 * pi / 4, pi / 4, pi / 2]) - - + def test_list(self): x = Twist2([1, 0, 0]) y = Twist2([1, 0, 0]) - + a = Twist2(x) a.append(y) self.assertEqual(len(a), 2) @@ -237,132 +245,126 @@ def test_list(self): array_compare(a[1], y) def test_variant_constructors(self): - # check rotational twist x = Twist2.UnitRevolute([1, 2]) array_compare(x, np.r_[2, -1, 1]) - + # check prismatic twist x = Twist2.UnitPrismatic([1, 2]) array_compare(x, np.r_[unitvec([1, 2]), 0]) - + def test_conversion_SE2(self): T = SE2(1, 2, 0.3) tw = Twist2(T) array_compare(tw.SE2(), T) self.assertIsInstance(tw.SE2(), SE2) self.assertEqual(len(tw.SE2()), 1) - + def test_conversion_se2(self): s = [1, 2, 3] x = Twist2(s) - - array_compare(x.skewa(), np.array([[ 0., -3., 1.], - [ 3., 0., 2.], - [ 0., 0., 0.]])) + + array_compare( + x.skewa(), np.array([[0.0, -3.0, 1.0], [3.0, 0.0, 2.0], [0.0, 0.0, 0.0]]) + ) def test_list_constuctor(self): x = Twist2([1, 0, 0]) - - a = Twist2([x,x,x,x]) + + a = Twist2([x, x, x, x]) self.assertIsInstance(a, Twist2) self.assertEqual(len(a), 4) - + a = Twist2([x.skewa(), x.skewa(), x.skewa(), x.skewa()]) self.assertIsInstance(a, Twist2) self.assertEqual(len(a), 4) - + a = Twist2([x.S, x.S, x.S, x.S]) self.assertIsInstance(a, Twist2) self.assertEqual(len(a), 4) - + s = np.r_[1, 2, 3] a = Twist2([s, s, s, s]) self.assertIsInstance(a, Twist2) self.assertEqual(len(a), 4) - + def test_predicate(self): x = Twist2.UnitRevolute([1, 2]) self.assertFalse(x.isprismatic) - + # check prismatic twist x = Twist2.UnitPrismatic([1, 2]) self.assertTrue(x.isprismatic) - + self.assertTrue(Twist2.isvalid(x.skewa())) self.assertTrue(Twist2.isvalid(x.S)) - + self.assertFalse(Twist2.isvalid(2)) self.assertFalse(Twist2.isvalid(np.eye(3))) - + def test_str(self): x = Twist2([1, 2, 3]) s = str(x) self.assertIsInstance(s, str) self.assertEqual(len(s), 8) - self.assertEqual(s.count('\n'), 0) - + self.assertEqual(s.count("\n"), 0) + x.append(x) s = str(x) self.assertIsInstance(s, str) self.assertEqual(len(s), 17) - self.assertEqual(s.count('\n'), 1) - + self.assertEqual(s.count("\n"), 1) def test_SE2_twists(self): - tw = Twist2( SE2() ) + tw = Twist2(SE2()) array_compare(tw, np.r_[0, 0, 0]) - - tw = Twist2( SE2(0, 0, pi / 2) ) + + tw = Twist2(SE2(0, 0, pi / 2)) array_compare(tw, np.r_[0, 0, pi / 2]) - - - tw = Twist2( SE2([1, 2, 0]) ) + + tw = Twist2(SE2([1, 2, 0])) array_compare(tw, [1, 2, 0]) - - tw = Twist2( SE2([1, 2, pi / 2])) - array_compare(tw, np.r_[ 3 * pi / 4, pi / 4, pi / 2]) - + + tw = Twist2(SE2([1, 2, pi / 2])) + array_compare(tw, np.r_[3 * pi / 4, pi / 4, pi / 2]) + def test_exp(self): x = Twist2.UnitRevolute([0, 0]) - array_compare(x.exp(pi/2), SE2(0, 0, pi/2)) - + array_compare(x.exp(pi / 2), SE2(0, 0, pi / 2)) + x = Twist2.UnitRevolute([1, 0]) - array_compare(x.exp(pi/2), SE2(1, -1, pi/2)) - + array_compare(x.exp(pi / 2), SE2(1, -1, pi / 2)) + x = Twist2.UnitRevolute([1, 2]) - array_compare(x.exp(pi/2), SE2(3, 1, pi/2)) + array_compare(x.exp(pi / 2), SE2(3, 1, pi / 2)) - def test_arith(self): - # check overloaded * T1 = SE2(1, 2, pi / 2) T2 = SE2(4, 5, -pi / 4) - + x1 = Twist2(T1) x2 = Twist2(T2) - array_compare( (x1 * x2).exp(), (T1 * T2).A) - array_compare( (x2 * x1).exp(), (T2 * T1).A) + array_compare((x1 * x2).exp(), (T1 * T2).A) + array_compare((x2 * x1).exp(), (T2 * T1).A) + + array_compare((x1 * x2).SE2(), (T1 * T2).A) + array_compare((x2 * x1).SE2(), (T2 * T1)) - array_compare( (x1 * x2).SE2(), (T1 * T2).A) - array_compare( (x2 * x1).SE2(), (T2 * T1)) - def test_prod(self): # check prod T1 = SE2(1, 2, pi / 2) T2 = SE2(4, 5, -pi / 4) - + x1 = Twist2(T1) x2 = Twist2(T2) - - x = Twist2([x1, x2]) - array_compare( x.prod().SE2(), T1 * T2) -# ---------------------------------------------------------------------------------------# -if __name__ == '__main__': + x = Twist2([x1, x2]) + array_compare(x.prod().SE2(), T1 * T2) +# ---------------------------------------------------------------------------------------# +if __name__ == "__main__": unittest.main() From 9c4a77bdc3b9f224da697911e271ca4e029b64cd Mon Sep 17 00:00:00 2001 From: Mark Yeatman <129521731+myeatman-bdai@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:11:16 -0500 Subject: [PATCH 13/14] Fix trailing whitespace and end of file lines. (#164) --- .github/CONTRIBUTORS.md | 2 +- .pre-commit-config.yaml | 15 +++++----- Makefile | 1 - README.md | 50 ++++++++++++++++---------------- spatialmath/base/README.md | 27 +++++++---------- spatialmath/base/argcheck.py | 2 +- spatialmath/base/symbolic.py | 2 +- spatialmath/base/transformsNd.py | 2 +- spatialmath/geom3d.py | 4 +-- spatialmath/spatialvector.py | 2 +- 10 files changed, 49 insertions(+), 58 deletions(-) diff --git a/.github/CONTRIBUTORS.md b/.github/CONTRIBUTORS.md index 30162fb8..80467523 100644 --- a/.github/CONTRIBUTORS.md +++ b/.github/CONTRIBUTORS.md @@ -3,4 +3,4 @@ A number of people have contributed to this, and earlier, versions of this Toolb * Jesse Haviland, 2020 (part of the ropy project) * Luis Fernando Lara Tobar, 2008 * Josh Carrigg Hodson, Aditya Dua, Chee Ho Chan, 2017 (part of the robopy project) -* Peter Corke \ No newline at end of file +* Peter Corke diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36085fe2..4dc48cd6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,17 +18,16 @@ repos: rev: v4.5.0 hooks: - id: end-of-file-fixer - - id: debug-statements # Ensure we don't commit `import pdb; pdb.set_trace()` exclude: | - (?x)^( - docker/ros/web/static/.*| - )$ + (?x)( + ^docs/ + ) + - id: debug-statements # Ensure we don't commit `import pdb; pdb.set_trace()` - id: trailing-whitespace exclude: | - (?x)^( - docker/ros/web/static/.*| - (.*/).*\.patch| - )$ + (?x)( + ^docs/ + ) # - repo: https://github.com/pre-commit/mirrors-mypy # rev: v1.6.1 # hooks: diff --git a/Makefile b/Makefile index c129744d..ad24ab91 100644 --- a/Makefile +++ b/Makefile @@ -39,4 +39,3 @@ clean: .FORCE (cd docs; make clean) -rm -r *.egg-info -rm -r dist build - diff --git a/README.md b/README.md index a2db78e4..738395e7 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ space: | ------------ | ---------------- | -------- | | pose | ``SE3`` ``Twist3`` ``UnitDualQuaternion`` | ``SE2`` ``Twist2`` | | orientation | ``SO3`` ``UnitQuaternion`` | ``SO2`` | - - + + More specifically: * `SE3` matrices belonging to the group $\mathbf{SE}(3)$ for position and orientation (pose) in 3-dimensions @@ -160,26 +160,26 @@ For example, to create an object representing a rotation of 0.3 radians about th >>> from spatialmath import SO3, SE3 >>> R1 = SO3.Rx(0.3) >>> R1 - 1 0 0 - 0 0.955336 -0.29552 - 0 0.29552 0.955336 + 1 0 0 + 0 0.955336 -0.29552 + 0 0.29552 0.955336 ``` while a rotation of 30 deg about the z-axis is ```python >>> R2 = SO3.Rz(30, 'deg') >>> R2 - 0.866025 -0.5 0 - 0.5 0.866025 0 - 0 0 1 + 0.866025 -0.5 0 + 0.5 0.866025 0 + 0 0 1 ``` -and the composition of these two rotations is +and the composition of these two rotations is ```python >>> R = R1 * R2 - 0.866025 -0.5 0 - 0.433013 0.75 -0.5 - 0.25 0.433013 0.866025 + 0.866025 -0.5 0 + 0.433013 0.75 -0.5 + 0.25 0.433013 0.866025 ``` We can find the corresponding Euler angles (in radians) @@ -198,16 +198,16 @@ Frequently in robotics we want a sequence, a trajectory, of rotation matrices or >>> len(R) 3 >>> R[1] - 1 0 0 - 0 0.955336 -0.29552 - 0 0.29552 0.955336 + 1 0 0 + 0 0.955336 -0.29552 + 0 0.29552 0.955336 ``` and this can be used in `for` loops and list comprehensions. An alternative way of constructing this would be (`R1`, `R2` defined above) ```python ->>> R = SO3( [ SO3(), R1, R2 ] ) +>>> R = SO3( [ SO3(), R1, R2 ] ) >>> len(R) 3 ``` @@ -233,7 +233,7 @@ will produce a result where each element is the product of each element of the l Similarly ```python ->>> A = SO3.Ry(0.5) * R +>>> A = SO3.Ry(0.5) * R >>> len(R) 32 ``` @@ -242,7 +242,7 @@ will produce a result where each element is the product of the left-hand side wi Finally ```python ->>> A = R * R +>>> A = R * R >>> len(R) 32 ``` @@ -267,10 +267,10 @@ We can print and plot these objects as well ``` >>> T = SE3(1,2,3) * SE3.Rx(30, 'deg') >>> T.print() - 1 0 0 1 - 0 0.866025 -0.5 2 - 0 0.5 0.866025 3 - 0 0 0 1 + 1 0 0 1 + 0 0.866025 -0.5 2 + 0 0.5 0.866025 3 + 0 0 0 1 >>> T.printline() t = 1, 2, 3; rpy/zyx = 30, 0, 0 deg @@ -339,7 +339,7 @@ array([[1., 0., 1.], [0., 0., 1.]]) transl2( (1,2) ) -Out[444]: +Out[444]: array([[1., 0., 1.], [0., 1., 2.], [0., 0., 1.]]) @@ -349,7 +349,7 @@ array([[1., 0., 1.], ``` transl2( np.array([1,2]) ) -Out[445]: +Out[445]: array([[1., 0., 1.], [0., 1., 2.], [0., 0., 1.]]) @@ -436,7 +436,7 @@ Out[259]: int a = T[1,1] a -Out[256]: +Out[256]: cos(theta) type(a) Out[255]: cos diff --git a/spatialmath/base/README.md b/spatialmath/base/README.md index a4003ffc..baa3595a 100644 --- a/spatialmath/base/README.md +++ b/spatialmath/base/README.md @@ -21,9 +21,9 @@ import spatialmath as sm R = sm.SO3.Rx(30, 'deg') print(R) - 1 0 0 - 0 0.866025 -0.5 - 0 0.5 0.866025 + 1 0 0 + 0 0.866025 -0.5 + 0 0.5 0.866025 ``` which constructs a rotation about the x-axis by 30 degrees. @@ -45,7 +45,7 @@ array([[ 1. , 0. , 0. ], [ 0. , 0.29552021, 0.95533649]]) >>> rotx(30, unit='deg') -Out[438]: +Out[438]: array([[ 1. , 0. , 0. ], [ 0. , 0.8660254, -0.5 ], [ 0. , 0.5 , 0.8660254]]) @@ -64,7 +64,7 @@ We also support multiple ways of passing vector information to functions that re ``` transl2(1, 2) -Out[442]: +Out[442]: array([[1., 0., 1.], [0., 1., 2.], [0., 0., 1.]]) @@ -74,13 +74,13 @@ array([[1., 0., 1.], ``` transl2( [1,2] ) -Out[443]: +Out[443]: array([[1., 0., 1.], [0., 1., 2.], [0., 0., 1.]]) transl2( (1,2) ) -Out[444]: +Out[444]: array([[1., 0., 1.], [0., 1., 2.], [0., 0., 1.]]) @@ -90,7 +90,7 @@ array([[1., 0., 1.], ``` transl2( np.array([1,2]) ) -Out[445]: +Out[445]: array([[1., 0., 1.], [0., 1., 2.], [0., 0., 1.]]) @@ -129,7 +129,7 @@ Using classes ensures type safety, for example it stops us mixing a 2D homogeneo These classes are all derived from two parent classes: * `RTBPose` which provides common functionality for all -* `UserList` which provdides the ability to act like a list +* `UserList` which provdides the ability to act like a list The latter is important because frequnetly in robotics we want a sequence, a trajectory, of rotation matrices or poses. However a list of these items has the type `list` and the elements are not enforced to be homogeneous, ie. a list could contain a mixture of classes. @@ -178,7 +178,7 @@ Out[259]: int a = T[1,1] a -Out[256]: +Out[256]: cos(theta) type(a) Out[255]: cos @@ -226,10 +226,3 @@ TypeError: can't convert expression to float | t2r | yes | | rotx | yes | | rotx | yes | - - - - - - - diff --git a/spatialmath/base/argcheck.py b/spatialmath/base/argcheck.py index 38b5eb1a..9db91817 100644 --- a/spatialmath/base/argcheck.py +++ b/spatialmath/base/argcheck.py @@ -5,7 +5,7 @@ """ Utility functions for testing and converting passed arguments. Used in all -spatialmath functions and classes to provides for flexibility in argument types +spatialmath functions and classes to provides for flexibility in argument types that can be passed. """ diff --git a/spatialmath/base/symbolic.py b/spatialmath/base/symbolic.py index 2d92f4d4..a95aec4a 100644 --- a/spatialmath/base/symbolic.py +++ b/spatialmath/base/symbolic.py @@ -8,7 +8,7 @@ Symbolic arguments. If SymPy is not installed then only the standard numeric operations are -supported. +supported. """ import math diff --git a/spatialmath/base/transformsNd.py b/spatialmath/base/transformsNd.py index c04e5d8b..611c89a3 100644 --- a/spatialmath/base/transformsNd.py +++ b/spatialmath/base/transformsNd.py @@ -514,7 +514,7 @@ def skew(v): if len(v) == 1: # fmt: off return np.array([ - [0.0, -v[0]], + [0.0, -v[0]], [v[0], 0.0] ]) # type: ignore # fmt: on diff --git a/spatialmath/geom3d.py b/spatialmath/geom3d.py index 8b191ebd..896192dc 100755 --- a/spatialmath/geom3d.py +++ b/spatialmath/geom3d.py @@ -507,7 +507,7 @@ def vec(self) -> R6: def skew(self) -> R4x4: r""" Line as a Plucker skew-symmetric matrix - + :return: Skew-symmetric matrix form of Plucker coordinates :rtype: ndarray(4,4) @@ -523,7 +523,7 @@ def skew(self) -> R4x4: -\omega_x & -\omega_y & -\omega_z & 0 \end{bmatrix} .. note:: - + - For two homogeneous points P and Q on the line, :math:`PQ^T-QP^T` is also skew symmetric. - The projection of Plucker line by a perspective camera is a diff --git a/spatialmath/spatialvector.py b/spatialmath/spatialvector.py index f839e359..0f996bee 100644 --- a/spatialmath/spatialvector.py +++ b/spatialmath/spatialvector.py @@ -10,7 +10,7 @@ :top-classes: collections.UserList :parts: 1 -.. note:: Compared to Featherstone's papers these spatial vectors have the +.. note:: Compared to Featherstone's papers these spatial vectors have the translational components first, followed by rotational components. """ From 4c68fa923bc90047a0d79a2eab5c5a84b6cee7b7 Mon Sep 17 00:00:00 2001 From: Mark Yeatman <129521731+myeatman-bdai@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:11:29 -0500 Subject: [PATCH 14/14] Bump version to 1.1.14. (#163) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 452bf290..54fc237c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "spatialmath-python" -version = "1.1.13" +version = "1.1.14" authors = [ { name="Peter Corke", email="rvc@petercorke.com" }, ] pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy