Skip to content

Commit d0324c5

Browse files
committed
Patched TSR wrapping to use Bw_cont and added a unit test.
1 parent 372db4f commit d0324c5

File tree

5 files changed

+71
-16
lines changed

5 files changed

+71
-16
lines changed

src/prpy/tsr/tsr.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@
3636
EPSILON = 0.001
3737

3838

39+
def wrap_to_interval(angles, lower=-pi):
40+
"""
41+
Wraps an angle into a semi-closed interval of width 2*pi.
42+
43+
By default, this interval is `[-pi, pi)`. However, the lower bound of the
44+
interval can be specified to wrap to the interval `[lower, lower + 2*pi)`.
45+
46+
If `lower` is an array the same length as angles, the bounds will be
47+
applied element-wise to each angle in `angles`.
48+
49+
See: http://stackoverflow.com/a/32266181
50+
51+
@param angles an angle or 1D array of angles to wrap
52+
@type angles float or numpy.array
53+
@param lower optional lower bound on wrapping interval
54+
@type lower float or numpy.array
55+
"""
56+
return (angles - lower) % (2*pi) + lower
57+
58+
3959
class TSR(object):
4060
""" A Task-Space-Region (TSR) represents a motion constraint. """
4161
def __init__(self, T0_w=None, Tw_e=None, Bw=None,
@@ -46,21 +66,24 @@ def __init__(self, T0_w=None, Tw_e=None, Bw=None,
4666
Tw_e = numpy.eye(4)
4767
if Bw is None:
4868
Bw = numpy.zeros((6, 2))
49-
if numpy.any(Bw[0:3, 0] > Bw[0:3, 1]):
50-
raise ValueError('Bw translation bounds must be [min, max]', Bw)
5169

52-
self.T0_w = T0_w
53-
self.Tw_e = Tw_e
54-
self.Bw = Bw
70+
self.T0_w = numpy.array(T0_w)
71+
self.Tw_e = numpy.array(Tw_e)
72+
self.Bw = numpy.array(Bw)
73+
74+
if numpy.any(self.Bw[0:3, 0] > self.Bw[0:3, 1]):
75+
raise ValueError('Bw translation bounds must be [min, max]', Bw)
5576

5677
# We will now create a continuous version of the bound to maintain:
5778
# 1. Bw[i,1] > Bw[i,0] which is necessary for LBFGS-B
5879
# 2. signed rotations, necessary for expressiveness
59-
Bw_cont = numpy.copy(Bw)
60-
Bw_cont[3:6, :] = (Bw_cont[3:6, :] + pi) % (2*pi) - pi
61-
for rot_idx in range(3, 6):
62-
if Bw_cont[rot_idx, 0] > Bw_cont[rot_idx, 1] + EPSILON:
63-
Bw_cont[rot_idx, 1] += 2*pi
80+
Bw_interval = Bw[3:6, 1] - Bw[3:6, 0]
81+
Bw_interval = numpy.minimum(Bw_interval, 2*pi)
82+
83+
Bw_cont = numpy.copy(self.Bw)
84+
Bw_cont[3:6, 0] = wrap_to_interval(Bw_cont[3:6, 0])
85+
Bw_cont[3:6, 1] = Bw_cont[3:6, 0] + Bw_interval
86+
6487
self._Bw_cont = Bw_cont
6588

6689
if manip is None:
@@ -175,8 +198,8 @@ def rpy_within_bounds(rpy, Bw):
175198
@param Bw bounds on rpy
176199
@return check a (3,) vector of True if within and False if outside
177200
"""
178-
# Unwrap rpy to [-pi, pi].
179-
rpy = (numpy.array(rpy) + pi) % (2*pi) - pi
201+
# Unwrap rpy to Bw_cont.
202+
rpy = wrap_to_interval(rpy, lower=Bw[:, 0])
180203

181204
# Check bounds condition on RPY component.
182205
rpycheck = [False] * 3
@@ -280,7 +303,7 @@ def to_xyzrpy(self, trans):
280303
trans,
281304
numpy.linalg.inv(self.Tw_e)])
282305
xyz, rot = Tw[0:3, 3], Tw[0:3, 0:3]
283-
rpycheck, rpy = TSR.rot_within_rpy_bounds(rot, self.Bw)
306+
rpycheck, rpy = TSR.rot_within_rpy_bounds(rot, self._Bw_cont)
284307
if not all(rpycheck):
285308
rpy = TSR.rot_to_rpy(rot)
286309
return numpy.hstack((xyz, rpy))
@@ -295,7 +318,7 @@ def is_valid(self, xyzrpy, ignoreNAN=False):
295318
@return a 6x1 vector of True if bound is valid and False if not
296319
"""
297320
# Extract XYZ and RPY components of input and TSR.
298-
Bw_xyz, Bw_rpy = self.Bw[0:3, :], self.Bw[3:6, :]
321+
Bw_xyz, Bw_rpy = self._Bw_cont[0:3, :], self._Bw_cont[3:6, :]
299322
xyz, rpy = xyzrpy[0:3], xyzrpy[3:6]
300323

301324
# Check bounds condition on XYZ component.
@@ -320,7 +343,7 @@ def contains(self, trans):
320343
@return a 6x1 vector of True if bound is valid and False if not
321344
"""
322345
# Extract XYZ and rot components of input and TSR.
323-
Bw_xyz, Bw_rpy = self.Bw[0:3, :], self.Bw[3:6, :]
346+
Bw_xyz, Bw_rpy = self._Bw_cont[0:3, :], self._Bw_cont[3:6, :]
324347
xyz, rot = trans[0:3, :], trans[0:3, 0:3]
325348
# Check bounds condition on XYZ component.
326349
xyzcheck = TSR.xyz_within_bounds(xyz, Bw_xyz)
@@ -374,7 +397,7 @@ def sample_xyzrpy(self, xyzrpy=NANBW):
374397
if numpy.isnan(x) else x
375398
for i, x in enumerate(xyzrpy)])
376399
# Unwrap rpy to [-pi, pi]
377-
Bw_sample[3:6] = (Bw_sample[3:6] + pi) % (2*pi) - pi
400+
Bw_sample[3:6] = wrap_to_interval(Bw_sample[3:6])
378401
return Bw_sample
379402

380403
def sample(self, xyzrpy=NANBW):

tests/__init__.py

Whitespace-only changes.

tests/planning/__init__.py

Whitespace-only changes.

tests/tsr/__init__.py

Whitespace-only changes.

tests/tsr/test_tsr.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import numpy
2+
from numpy import pi
3+
from prpy.tsr import Tsr
4+
from unittest import TestCase
5+
6+
7+
class TsrTest(TestCase):
8+
""" Test cases for prpy.tsr.Tsr. """
9+
def test_sample_xyzrpy(self):
10+
# Test zero-intervals.
11+
Bw = [[0., 0.], # X
12+
[1., 1.], # Y
13+
[-1., -1.], # Z
14+
[0., 0.], # roll
15+
[pi, pi], # pitch
16+
[-pi, -pi]] # yaw
17+
tsr = Tsr(Bw=Bw)
18+
s = tsr.sample_xyzrpy()
19+
self.assertTrue(numpy.all(s >= Bw[:, 0]))
20+
self.assertTrue(numpy.all(s <= Bw[:, 1]))
21+
22+
# Test over-wrapped angle intervals.
23+
Bw = [[0., 0.], # X
24+
[0., 0.], # Y
25+
[0., 0.], # Z
26+
[pi, 3.*pi], # roll
27+
[pi/2., 3*pi/2.], # pitch
28+
[-3*pi/2., -pi/2.]] # yaw
29+
tsr = Tsr(Bw=Bw)
30+
s = tsr.sample_xyzrpy()
31+
self.assertTrue(numpy.all(s >= Bw[:, 0]))
32+
self.assertTrue(numpy.all(s <= Bw[:, 1]))

0 commit comments

Comments
 (0)