Skip to content

Commit d1a945d

Browse files
RoboX2020taughz
andauthored
Accept a stacked (N, r, c) ndarray as a sequence of poses (SE3, SO3, SE2, SO2) (#243)
Closes #236. `SE3(arr)` with an `(N, 4, 4)` array raised "bad argument to constructor", and with `check=False` it returned a malformed object (length 1, but `.A` of shape `(N, 4, 4)`). Changes: - `BasePoseList.arghandler`: an `ndarray` with shape `(N, r, c)`, where `(r, c)` is the shape of one value of the class, is now taken as N values, the same as `SE3(list(arr))`. Each matrix still goes through the class's validity check when `check=True`. This covers the matrix classes `SE3`, `SO3`, `SE2` and `SO2`. - With `check=False`, single arrays keep their historical behavior; only a stacked 3D array whose per-matrix size is wrong is rejected, whatever `check` is. - The `SE3` error message now lists the accepted forms, and the constructor docstring documents the new form. Test: `test_constructor_stacked_array` covers `SE3` and `SO3` from a stacked array with both `check` values, the empty `(0, 4, 4)` case, rejection of invalid matrices with `check=True` and of a wrong-sized stack with either value. It fails on `master` and passes with this change. Run locally (Python 3.11): `pytest tests` gives 348 passed, 3 skipped. black 23.10.0 is clean. Not run: the docs build and other Python versions. --------- Co-authored-by: Tim Perkins <tim@rai-inst.com>
1 parent 80fca19 commit d1a945d

5 files changed

Lines changed: 70 additions & 1 deletion

File tree

‎spatialmath/baseposelist.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ def arghandler(
154154
155155
#. None, an identity value is created
156156
#. a numpy.ndarray of the appropriate shape and value which is valid for the subclass
157+
#. a stacked numpy.ndarray of shape (N, r, c), where (r, c) is the shape of one value
158+
of the subclass, which is a sequence of N values
157159
#. a list whose elements all meet the criteria above
158160
#. an instance of the subclass
159161
#. a list whose elements are all singelton instances of the subclass
@@ -187,6 +189,20 @@ def arghandler(
187189
elif isinstance(arg, np.ndarray):
188190
# it's a numpy array
189191

192+
shape = tuple(self.shape)
193+
if len(shape) == 2 and arg.ndim == 3:
194+
# a 3D array passed to a matrix pose class is a stack
195+
if arg.shape[1:] != shape:
196+
# a stack of matrices of the wrong size is rejected,
197+
# whatever the value of check
198+
return False
199+
# a stacked (N, r, c) array is a sequence of N values
200+
values = [self._import(x, check=check) for x in arg]
201+
if any(x is None for x in values):
202+
return False
203+
self.data = values
204+
return True
205+
190206
x = self._import(arg, check=check)
191207
if x is not None:
192208
self.data = [x]

‎spatialmath/pose3d.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,9 @@ def __init__(self, x=None, y=None, z=None, *, check=True):
10961096
- ``SE3([X1, X2, ... XN])`` has ``N`` values
10971097
given by the elements ``Xi`` each of which is an SE3 instance.
10981098
1099+
- ``SE3(A)`` where ``A`` is an ``(N, 4, 4)`` NumPy array has ``N`` values,
1100+
equivalent to ``SE3(list(A))``.
1101+
10991102
:SymPy: supported
11001103
"""
11011104
if y is None and z is None:
@@ -1115,7 +1118,10 @@ def __init__(self, x=None, y=None, z=None, *, check=True):
11151118
self.data = [smb.transl(T) for T in x]
11161119

11171120
else:
1118-
raise ValueError("bad argument to constructor")
1121+
raise ValueError(
1122+
"bad argument to constructor: expected an SE3, SO3 or SE2 instance, a 4x4 or "
1123+
"(N, 4, 4) array, a list of those, a 3-vector or an (N, 3) array of translations"
1124+
)
11191125

11201126
elif y is not None and z is not None:
11211127
# SE3(x, y, z)

‎tests/test_pose3d.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,26 @@ class TestSE3(unittest.TestCase):
793793
def tearDownClass(cls):
794794
plt.close("all")
795795

796+
def test_constructor_stacked_array(self):
797+
# an (N, 4, 4) ndarray is a sequence of N poses, like a list of 4x4 arrays
798+
poses = [SE3.Rx(0.3), SE3(1, 2, 3) * SE3.Ry(0.4), SE3.Rz(-0.2)]
799+
stacked = np.array([T.A for T in poses])
800+
for check in (True, False):
801+
T = SE3(stacked, check=check)
802+
self.assertEqual(len(T), 3)
803+
for k in range(3):
804+
nt.assert_array_almost_equal(T[k].A, poses[k].A)
805+
self.assertEqual(len(SE3(np.empty((0, 4, 4)))), 0)
806+
self.assertEqual(len(SO3(np.array([SO3.Rx(0.1).A, SO3.Ry(0.2).A]))), 2)
807+
808+
# stacked matrices that are not valid poses are still rejected by check=True
809+
with self.assertRaises(ValueError):
810+
SE3(2 * stacked)
811+
# and a stack of matrices of the wrong size is rejected whatever the value of check
812+
for check in (True, False):
813+
with self.assertRaises(ValueError):
814+
SE3(np.ones((3, 3, 3)), check=check)
815+
796816
def test_constructor(self):
797817
# null constructor
798818
R = SE3()

‎tests/test_quaternion.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,9 @@ def test_constructor(self):
954954
# tc.verifyError( @() Quaternion(2), 'SMTB:Quaternion:badarg')
955955
# tc.verifyError( @() Quaternion([1, 2, 3]), 'SMTB:Quaternion:badarg')
956956

957+
# an array that is not a 4-vector must not give an empty Quaternion
958+
self.assertNotEqual(len(Quaternion(np.random.rand(3, 4))), 0)
959+
957960
def test_string(self):
958961
u = Quaternion()
959962

‎tests/test_twist.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ def test_conversion_SE3(self):
6767
T = SE3.Rx(0) * SE3(1, 2, 3)
6868
array_compare(Twist3(T).SE3(), T)
6969

70+
# from an SE(3) matrix, with and without checking
71+
for check in (True, False):
72+
tw = Twist3(T.A, check=check)
73+
self.assertEqual(len(tw), 1)
74+
array_compare(tw.SE3(), T)
75+
7076
def test_conversion_se3(self):
7177
s = [1, 2, 3, 4, 5, 6]
7278
x = Twist3(s)
@@ -83,6 +89,12 @@ def test_conversion_se3(self):
8389
),
8490
)
8591

92+
# from an se(3) matrix, with and without checking
93+
for check in (True, False):
94+
tw = Twist3(x.skewa(), check=check)
95+
self.assertEqual(len(tw), 1)
96+
array_compare(tw, x)
97+
8698
def test_conversion_Plucker(self):
8799
pass
88100

@@ -319,6 +331,12 @@ def test_conversion_SE2(self):
319331
self.assertIsInstance(tw.SE2(), SE2)
320332
self.assertEqual(len(tw.SE2()), 1)
321333

334+
# from an SE(2) matrix, with and without checking
335+
for check in (True, False):
336+
tw = Twist2(T.A, check=check)
337+
self.assertEqual(len(tw), 1)
338+
array_compare(tw.SE2(), T)
339+
322340
def test_conversion_se2(self):
323341
s = [1, 2, 3]
324342
x = Twist2(s)
@@ -327,6 +345,12 @@ def test_conversion_se2(self):
327345
x.skewa(), np.array([[0.0, -3.0, 1.0], [3.0, 0.0, 2.0], [0.0, 0.0, 0.0]])
328346
)
329347

348+
# from an se(2) matrix, with and without checking
349+
for check in (True, False):
350+
tw = Twist2(x.skewa(), check=check)
351+
self.assertEqual(len(tw), 1)
352+
array_compare(tw, x)
353+
330354
def test_list_constuctor(self):
331355
x = Twist2([1, 0, 0])
332356

0 commit comments

Comments
 (0)