Skip to content

Commit b08ee7e

Browse files
committed
Merge pull request #194 from personalrobotics/bugfix/tsr_wrapping
Patched TSR wrapping to use Bw_cont and added TSR unit test.
2 parents 37cbea0 + c1ca39c commit b08ee7e

File tree

5 files changed

+76
-16
lines changed

5 files changed

+76
-16
lines changed

src/prpy/tsr/tsr.py

Lines changed: 40 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,25 @@ 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_cont = numpy.copy(self.Bw)
81+
82+
Bw_interval = Bw_cont[3:6, 1] - Bw_cont[3:6, 0]
83+
Bw_interval = numpy.minimum(Bw_interval, 2*pi)
84+
85+
Bw_cont[3:6, 0] = wrap_to_interval(Bw_cont[3:6, 0])
86+
Bw_cont[3:6, 1] = Bw_cont[3:6, 0] + Bw_interval
87+
6488
self._Bw_cont = Bw_cont
6589

6690
if manip is None:
@@ -175,8 +199,8 @@ def rpy_within_bounds(rpy, Bw):
175199
@param Bw bounds on rpy
176200
@return check a (3,) vector of True if within and False if outside
177201
"""
178-
# Unwrap rpy to [-pi, pi].
179-
rpy = (numpy.array(rpy) + pi) % (2*pi) - pi
202+
# Unwrap rpy to Bw_cont.
203+
rpy = wrap_to_interval(rpy, lower=Bw[:, 0])
180204

181205
# Check bounds condition on RPY component.
182206
rpycheck = [False] * 3
@@ -280,7 +304,7 @@ def to_xyzrpy(self, trans):
280304
trans,
281305
numpy.linalg.inv(self.Tw_e)])
282306
xyz, rot = Tw[0:3, 3], Tw[0:3, 0:3]
283-
rpycheck, rpy = TSR.rot_within_rpy_bounds(rot, self.Bw)
307+
rpycheck, rpy = TSR.rot_within_rpy_bounds(rot, self._Bw_cont)
284308
if not all(rpycheck):
285309
rpy = TSR.rot_to_rpy(rot)
286310
return numpy.hstack((xyz, rpy))
@@ -295,7 +319,7 @@ def is_valid(self, xyzrpy, ignoreNAN=False):
295319
@return a 6x1 vector of True if bound is valid and False if not
296320
"""
297321
# Extract XYZ and RPY components of input and TSR.
298-
Bw_xyz, Bw_rpy = self.Bw[0:3, :], self.Bw[3:6, :]
322+
Bw_xyz, Bw_rpy = self._Bw_cont[0:3, :], self._Bw_cont[3:6, :]
299323
xyz, rpy = xyzrpy[0:3], xyzrpy[3:6]
300324

301325
# Check bounds condition on XYZ component.
@@ -320,7 +344,7 @@ def contains(self, trans):
320344
@return a 6x1 vector of True if bound is valid and False if not
321345
"""
322346
# Extract XYZ and rot components of input and TSR.
323-
Bw_xyz, Bw_rpy = self.Bw[0:3, :], self.Bw[3:6, :]
347+
Bw_xyz, Bw_rpy = self._Bw_cont[0:3, :], self._Bw_cont[3:6, :]
324348
xyz, rot = trans[0:3, :], trans[0:3, 0:3]
325349
# Check bounds condition on XYZ component.
326350
xyzcheck = TSR.xyz_within_bounds(xyz, Bw_xyz)
@@ -374,7 +398,7 @@ def sample_xyzrpy(self, xyzrpy=NANBW):
374398
if numpy.isnan(x) else x
375399
for i, x in enumerate(xyzrpy)])
376400
# Unwrap rpy to [-pi, pi]
377-
Bw_sample[3:6] = (Bw_sample[3:6] + pi) % (2*pi) - pi
401+
Bw_sample[3:6] = wrap_to_interval(Bw_sample[3:6])
378402
return Bw_sample
379403

380404
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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
20+
Bw = numpy.array(Bw)
21+
self.assertTrue(numpy.all(s >= Bw[:, 0]))
22+
self.assertTrue(numpy.all(s <= Bw[:, 1]))
23+
24+
# Test over-wrapped angle intervals.
25+
Bw = [[0., 0.], # X
26+
[0., 0.], # Y
27+
[0., 0.], # Z
28+
[pi, 3.*pi], # roll
29+
[pi/2., 3*pi/2.], # pitch
30+
[-3*pi/2., -pi/2.]] # yaw
31+
tsr = TSR(Bw=Bw)
32+
s = tsr.sample_xyzrpy()
33+
34+
Bw = numpy.array(Bw)
35+
self.assertTrue(numpy.all(s >= Bw[:, 0]))
36+
self.assertTrue(numpy.all(s <= Bw[:, 1]))

0 commit comments

Comments
 (0)