Skip to content

Commit 5bd2e34

Browse files
authored
Keep twist poles invariant under angular speed scaling (#249)
`Twist2.pole` and `Twist3.pole` divide the cross product by the angular magnitude. The closest point on the screw axis requires its square: the cross product already contains one factor of angular speed. Consequently, scaling a twist currently moves its reported axis. ```python from spatialmath import Twist2, Twist3 print(Twist2([1, -2, 0.5]).pole) # expected [4, 2]; currently [2, 1] print(Twist3([1, -2, 0, 0, 0, 0.5]).pole) # expected [4, 2, 0] ``` Use `w**2` in 2D and `dot(w, w)` in 3D. The zero-angular case keeps its existing undefined behavior. The regression checks cover signed speeds, off-origin axes and nonzero screw pitch. They verify that finite motion leaves an axis point fixed except for translation along the axis. Twelve of the 15 cases fail before the fix. On a camera-reconstruction fixture, 680 incorrect axis estimates become zero across 768 scaled 2D/3D increments; independent least-squares axis references agree within `1.5e-14`. Ordinary transform outputs remain identical. Full suite: 362 passed, 3 existing skips; project pre-commit hooks pass. Linux/Python 3.12.
1 parent 7be780b commit 5bd2e34

2 files changed

Lines changed: 37 additions & 2 deletions

File tree

‎spatialmath/twist.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,7 @@ def pole(self):
986986
>>> S = Twist3(T)
987987
>>> S.pole
988988
"""
989-
return np.cross(self.w, self.v) / self.theta
989+
return np.cross(self.w, self.v) / np.dot(self.w, self.w)
990990

991991
def SE3(self, theta=1, unit="rad"):
992992
"""
@@ -1502,7 +1502,7 @@ def pole(self):
15021502
>>> S.pole
15031503
15041504
"""
1505-
p = np.cross(np.r_[0, 0, self.w], np.r_[self.v, 0]) / self.theta
1505+
p = np.cross(np.r_[0, 0, self.w], np.r_[self.v, 0]) / self.w**2
15061506
return p[:2]
15071507

15081508
# ------------------------- methods -------------------------------#

‎tests/test_twist_pole.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import numpy as np
2+
import pytest
3+
from scipy.linalg import expm
4+
5+
from spatialmath import Twist2, Twist3
6+
7+
8+
@pytest.mark.parametrize("speed", [-2.0, -0.3, 0.3, 1.0, 2.0])
9+
def test_twist2_pole_is_rotation_center(speed):
10+
"""A change in angular speed must not move the center of rotation."""
11+
center = np.array([3.0, -2.0])
12+
twist = Twist2(speed * np.r_[center[1], -center[0], 1])
13+
np.testing.assert_allclose(twist.pole, center)
14+
transform = expm(twist.skewa() * 0.7)
15+
np.testing.assert_allclose(
16+
transform @ np.r_[twist.pole, 1], np.r_[center, 1], atol=1e-14
17+
)
18+
19+
20+
@pytest.mark.parametrize("speed", [-2.0, -0.3, 0.3, 1.0, 2.0])
21+
@pytest.mark.parametrize("pitch", [0, 0.4])
22+
def test_twist3_pole_is_on_screw_axis(speed, pitch):
23+
"""The closest axis point is fixed except for translation along the axis."""
24+
axis = np.array([1.0, 2.0, -2.0]) / 3
25+
center = np.array([3.0, -2.0, 1.0])
26+
center -= (center @ axis) * axis
27+
angular = speed * axis
28+
linear = -np.cross(angular, center) + pitch * angular
29+
twist = Twist3(linear, angular)
30+
np.testing.assert_allclose(twist.pole, center)
31+
transform = expm(twist.skewa() * 0.7)
32+
expected = center + pitch * angular * 0.7
33+
np.testing.assert_allclose(
34+
(transform @ np.r_[twist.pole, 1])[:3], expected, atol=1e-14
35+
)

0 commit comments

Comments
 (0)