Skip to content

Commit d7e69da

Browse files
antonsootaughz
authored andcommitted
Broadcast scalar integration steps over twist sequences
1 parent 41e0a4d commit d7e69da

2 files changed

Lines changed: 61 additions & 4 deletions

File tree

‎spatialmath/twist.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,7 @@ def SE3(self, theta=1, unit="rad"):
10141014

10151015
if len(theta) == 1:
10161016
# theta is a scalar
1017-
return SE3(smb.trexp(self.S * theta))
1017+
return SE3([smb.trexp(S * theta[0]) for S in self.data])
10181018
else:
10191019
# theta is a vector
10201020
if len(self) == 1:
@@ -1072,6 +1072,8 @@ def exp(self, theta=1, unit="rad"):
10721072

10731073
if len(self) == 1:
10741074
return SE3([smb.trexp(self.S * t) for t in theta], check=False)
1075+
elif len(theta) == 1:
1076+
return SE3([smb.trexp(s * theta[0]) for s in self.data], check=False)
10751077
elif len(self) == len(theta):
10761078
return SE3([smb.trexp(s * t) for s, t in zip(self.S, theta)], check=False)
10771079
else:
@@ -1530,15 +1532,19 @@ def SE2(self, theta=1, unit="rad"):
15301532
"""
15311533
from spatialmath.pose2d import SE2
15321534

1533-
if unit != "rad" and self.isprismatic:
1535+
if unit != "rad" and any(s.isprismatic for s in self):
15341536
print("Twist3.exp: using degree mode for a prismatic twist")
15351537

15361538
theta = smb.getunit(theta, unit)
15371539

15381540
if len(theta) == 1:
1539-
return SE2(smb.trexp2(self.S * theta))
1540-
else:
1541+
return SE2([smb.trexp2(S * theta[0]) for S in self.data])
1542+
elif len(self) == 1:
15411543
return SE2([smb.trexp2(self.S * t) for t in theta])
1544+
elif len(self) == len(theta):
1545+
return SE2([smb.trexp2(S * t) for S, t in zip(self.data, theta)])
1546+
else:
1547+
raise ValueError("length of twist and theta not consistent")
15421548

15431549
def skewa(self):
15441550
"""
@@ -1605,6 +1611,8 @@ def exp(self, theta=1, unit="rad"):
16051611

16061612
if len(self) == 1:
16071613
return SE2([smb.trexp2(self.S * t) for t in theta], check=False)
1614+
elif len(theta) == 1:
1615+
return SE2([smb.trexp2(s * theta[0]) for s in self.data], check=False)
16081616
elif len(self) == len(theta):
16091617
return SE2([smb.trexp2(s * t) for s, t in zip(self.S, theta)], check=False)
16101618
else:

‎tests/test_twist_sequence.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import numpy as np
2+
import pytest
3+
from scipy.linalg import expm
4+
5+
from spatialmath import SE2, SE3, Twist2, Twist3
6+
7+
8+
@pytest.mark.parametrize("twist,pose", [(Twist2, SE2), (Twist3, SE3)])
9+
@pytest.mark.parametrize("method", ["exp", "pose"])
10+
@pytest.mark.parametrize("theta", [None, 0, -0.4, [0.2, -0.7]])
11+
def test_twist_sequence_exponential(twist, pose, method, theta):
12+
"""A shared step and individual steps both integrate every twist."""
13+
vectors = (
14+
[[1, -2, 0.3], [2, 1, -0.4]]
15+
if twist is Twist2
16+
else [[1, -2, 3, 0.2, 0.3, -0.4], [2, 1, -1, -0.1, 0.5, 0.2]]
17+
)
18+
sequence = twist([np.array(v) for v in vectors])
19+
convert = getattr(sequence, "exp" if method == "exp" else pose.__name__)
20+
result = convert() if theta is None else convert(theta)
21+
steps = np.broadcast_to(1 if theta is None else theta, (2,))
22+
assert isinstance(result, pose)
23+
assert len(result) == 2
24+
for value, source, step in zip(result, sequence, steps):
25+
np.testing.assert_allclose(value.A, expm(source.skewa() * step), atol=1e-14)
26+
27+
28+
@pytest.mark.parametrize("twist,pose", [(Twist2, SE2), (Twist3, SE3)])
29+
@pytest.mark.parametrize("method", ["exp", "pose"])
30+
def test_twist_sequence_degrees_and_singleton(twist, pose, method):
31+
"""Degrees, singleton expansion and mismatched lengths retain their rules."""
32+
source = twist([1, 2, 0.3] if twist is Twist2 else [1, 2, 3, 0.2, 0.3, 0.4])
33+
name = "exp" if method == "exp" else pose.__name__
34+
angles = [15, -30]
35+
expanded = getattr(source, name)(angles, unit="deg")
36+
assert len(expanded) == 2
37+
for value, angle in zip(expanded, angles):
38+
np.testing.assert_allclose(
39+
value.A, expm(source.skewa() * np.deg2rad(angle)), atol=1e-14
40+
)
41+
sequence = twist([source, source])
42+
shared = getattr(sequence, name)(30, unit="deg")
43+
assert len(shared) == 2
44+
for value in shared:
45+
np.testing.assert_allclose(
46+
value.A, expm(source.skewa() * np.deg2rad(30)), atol=1e-14
47+
)
48+
with pytest.raises(ValueError):
49+
getattr(sequence, name)([0.1, 0.2, 0.3])

0 commit comments

Comments
 (0)