blob: 3b5048e3d741b502fa465b3a5b798569a2e750e2 [file] [log] [blame]
milind-u18a901d2023-02-17 21:51:55 -08001import abc
2import numpy as np
3import sys
4import traceback
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -08005
6# joint_center in x-y space.
milind-u18a901d2023-02-17 21:51:55 -08007IN_TO_M = 0.0254
8joint_center = (-0.203, 0.787)
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -08009
10# Joint distances (l1 = "proximal", l2 = "distal")
milind-u18a901d2023-02-17 21:51:55 -080011l1 = 20.0 * IN_TO_M
12l2 = 31.5 * IN_TO_M
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -080013
14max_dist = 0.01
milind-u18a901d2023-02-17 21:51:55 -080015max_dist_theta = np.pi / 64
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -080016xy_end_circle_size = 0.01
17theta_end_circle_size = 0.07
18
19
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -080020# Convert from x-y coordinates to theta coordinates.
21# orientation is a bool. This orientation is circular_index mod 2.
22# where circular_index is the circular index, or the position in the
23# "hyperextension" zones. "cross_point" allows shifting the place where
24# it rounds the result so that it draws nicer (no other functional differences).
milind-u18a901d2023-02-17 21:51:55 -080025def to_theta(pt, circular_index, cross_point=-np.pi):
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -080026 orient = (circular_index % 2) == 0
27 x = pt[0]
28 y = pt[1]
29 x -= joint_center[0]
30 y -= joint_center[1]
milind-u18a901d2023-02-17 21:51:55 -080031 l3 = np.hypot(x, y)
32 t3 = np.arctan2(y, x)
33 theta1 = np.arccos((l1**2 + l3**2 - l2**2) / (2 * l1 * l3))
34 if np.isnan(theta1):
35 traceback.print_stack()
36 sys.exit("Couldn't fit triangle to %f, %f, %f" % (l1, l2, l3))
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -080037
38 if orient:
39 theta1 = -theta1
40 theta1 += t3
milind-u18a901d2023-02-17 21:51:55 -080041 theta1 = (theta1 - cross_point) % (2 * np.pi) + cross_point
42 theta2 = np.arctan2(y - l1 * np.sin(theta1), x - l1 * np.cos(theta1))
43 return np.array((theta1, theta2))
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -080044
45
46# Simple trig to go back from theta1, theta2 to x-y
47def to_xy(theta1, theta2):
milind-u18a901d2023-02-17 21:51:55 -080048 x = np.cos(theta1) * l1 + np.cos(theta2) * l2 + joint_center[0]
49 y = np.sin(theta1) * l1 + np.sin(theta2) * l2 + joint_center[1]
50 orient = ((theta2 - theta1) % (2.0 * np.pi)) < np.pi
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -080051 return (x, y, orient)
52
53
milind-u18a901d2023-02-17 21:51:55 -080054END_EFFECTOR_X_LEN = (-1.0 * IN_TO_M, 10.425 * IN_TO_M)
55END_EFFECTOR_Y_LEN = (-4.875 * IN_TO_M, 7.325 * IN_TO_M)
56END_EFFECTOR_Z_LEN = (-11.0 * IN_TO_M, 11.0 * IN_TO_M)
57
58
59def abs_sum(l):
60 result = 0
61 for e in l:
62 result += abs(e)
63 return result
64
65
66def affine_3d(R, T):
67 H = np.eye(4)
68 H[:3, 3] = T
69 H[:3, :3] = R
70 return H
71
72
73# Simple trig to go back from theta1, theta2, and theta3 to
74# the 8 corners on the roll joint x-y-z
75def to_end_effector_points(theta1, theta2, theta3):
76 x, y, _ = to_xy(theta1, theta2)
77 # Homogeneous end effector points relative to the end_effector
78 # ee = end effector
79 endpoints_ee = []
80 for i in range(2):
81 for j in range(2):
82 for k in range(2):
83 endpoints_ee.append(
84 np.array((END_EFFECTOR_X_LEN[i], END_EFFECTOR_Y_LEN[j],
85 END_EFFECTOR_Z_LEN[k], 1.0)))
86
87 # Only roll.
88 # rj = roll joint
89 roll = theta3
90 T_rj_ee = np.zeros(3)
91 R_rj_ee = np.array([[1.0, 0.0, 0.0], [0.0,
92 np.cos(roll), -np.sin(roll)],
93 [0.0, np.sin(roll), np.cos(roll)]])
94 H_rj_ee = affine_3d(R_rj_ee, T_rj_ee)
95
96 # Roll joint pose relative to the origin
97 # o = origin
98 T_o_rj = np.array((x, y, 0))
99 # Only yaw
100 yaw = theta1 + theta2
101 R_o_rj = [[np.cos(yaw), -np.sin(yaw), 0.0],
102 [np.sin(yaw), np.cos(yaw), 0.0], [0.0, 0.0, 1.0]]
103 H_o_rj = affine_3d(R_o_rj, T_o_rj)
104
105 # Now compute the pose of the end effector relative to the origin
106 H_o_ee = H_o_rj @ H_rj_ee
107
108 # Get the translation from these transforms
109 endpoints_o = [(H_o_ee @ endpoint_ee)[:3] for endpoint_ee in endpoints_ee]
110
111 diagonal_distance = np.linalg.norm(
112 np.array(endpoints_o[0]) - np.array(endpoints_o[-1]))
113 actual_diagonal_distance = np.linalg.norm(
114 np.array((abs_sum(END_EFFECTOR_X_LEN), abs_sum(END_EFFECTOR_Y_LEN),
115 abs_sum(END_EFFECTOR_Z_LEN))))
116 assert abs(diagonal_distance - actual_diagonal_distance) < 1e-5
117
118 return np.array(endpoints_o)
119
120
121# Returns all permutations of rectangle points given two opposite corners.
122# x is the two x values, y is the two y values, z is the two z values
123def rect_points(x, y, z):
124 points = []
125 for i in range(2):
126 for j in range(2):
127 for k in range(2):
128 points.append((x[i], y[j], z[k]))
129 return np.array(points)
130
131
132DRIVER_CAM_Z_OFFSET = 3.225 * IN_TO_M
133DRIVER_CAM_POINTS = rect_points(
134 (-5.126 * IN_TO_M + joint_center[0], 0.393 * IN_TO_M + joint_center[0]),
135 (5.125 * IN_TO_M + joint_center[1], 17.375 * IN_TO_M + joint_center[1]),
136 (-8.475 * IN_TO_M - DRIVER_CAM_Z_OFFSET,
137 -4.350 * IN_TO_M - DRIVER_CAM_Z_OFFSET))
138
139
140def compute_face_normals(points):
141 # Return the normal vectors of all the faces
142 normals = []
143 for i in range(points.shape[0]):
144 v1 = points[i]
145 v2 = points[(i + 1) % points.shape[0]]
146 normal = np.cross(v1, v2)
147 normals.append(normal)
148 return np.array(normals)
149
150
151def project_points_onto_axis(points, axis):
152 projections = np.dot(points, axis)
153 return np.min(projections), np.max(projections)
154
155
156def roll_joint_collision(theta1, theta2, theta3):
157 end_effector_points = to_end_effector_points(theta1, theta2, theta3)
158
159 assert len(end_effector_points) == 8 and len(end_effector_points[0]) == 3
160 assert len(DRIVER_CAM_POINTS) == 8 and len(DRIVER_CAM_POINTS[0]) == 3
161
162 # Use the Separating Axis Theorem to check for collision
163 end_effector_normals = compute_face_normals(end_effector_points)
164 driver_cam_normals = compute_face_normals(DRIVER_CAM_POINTS)
165
166 collision = True
167 # Check for separating axes
168 for normal in np.concatenate((end_effector_normals, driver_cam_normals)):
169 min_ee, max_ee = project_points_onto_axis(end_effector_points, normal)
170 min_dc, max_dc = project_points_onto_axis(DRIVER_CAM_POINTS, normal)
171 if max_ee < min_dc or min_ee > max_dc:
172 # Separating axis found, rectangles don't intersect
173 collision = False
174 break
175
176 return collision
177
178
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800179def get_circular_index(theta):
milind-u18a901d2023-02-17 21:51:55 -0800180 return int(np.floor((theta[1] - theta[0]) / np.pi))
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800181
182
183def get_xy(theta):
184 theta1 = theta[0]
185 theta2 = theta[1]
milind-u18a901d2023-02-17 21:51:55 -0800186 x = np.cos(theta1) * l1 + np.cos(theta2) * l2 + joint_center[0]
187 y = np.sin(theta1) * l1 + np.sin(theta2) * l2 + joint_center[1]
188 return np.array((x, y))
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800189
190
191# Subdivide in theta space.
192def subdivide_theta(lines):
193 out = []
194 last_pt = lines[0]
195 out.append(last_pt)
196 for n_pt in lines[1:]:
197 for pt in subdivide(last_pt, n_pt, max_dist_theta):
198 out.append(pt)
199 last_pt = n_pt
200
201 return out
202
203
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800204def to_theta_with_ci(pt, circular_index):
milind-u18a901d2023-02-17 21:51:55 -0800205 return (to_theta_with_circular_index(pt[0], pt[1], circular_index))
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800206
207
208# to_theta, but distinguishes between
209def to_theta_with_circular_index(x, y, circular_index):
210 theta1, theta2 = to_theta((x, y), circular_index)
milind-u18a901d2023-02-17 21:51:55 -0800211 n_circular_index = int(np.floor((theta2 - theta1) / np.pi))
212 theta2 = theta2 + ((circular_index - n_circular_index)) * np.pi
213 return np.array((theta1, theta2))
214
215
216# to_theta, but distinguishes between
217def to_theta_with_circular_index_and_roll(x, y, roll, circular_index):
218 theta12 = to_theta_with_circular_index(x, y, circular_index)
219 theta3 = roll
220 return np.array((theta12[0], theta12[1], theta3))
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800221
222
223# alpha is in [0, 1] and is the weight to merge a and b.
224def alpha_blend(a, b, alpha):
225 """Blends a and b.
226
227 Args:
228 alpha: double, Ratio. Needs to be in [0, 1] and is the weight to blend a
229 and b.
230 """
231 return b * alpha + (1.0 - alpha) * a
232
233
234def normalize(v):
235 """Normalize a vector while handling 0 length vectors."""
milind-u18a901d2023-02-17 21:51:55 -0800236 norm = np.linalg.norm(v)
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800237 if norm == 0:
238 return v
239 return v / norm
240
241
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800242# Generic subdivision algorithm.
243def subdivide(p1, p2, max_dist):
244 dx = p2[0] - p1[0]
245 dy = p2[1] - p1[1]
milind-u18a901d2023-02-17 21:51:55 -0800246 dist = np.sqrt(dx**2 + dy**2)
247 n = int(np.ceil(dist / max_dist))
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800248 return [(alpha_blend(p1[0], p2[0],
249 float(i) / n), alpha_blend(p1[1], p2[1],
250 float(i) / n))
251 for i in range(1, n + 1)]
252
253
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800254def spline_eval(start, control1, control2, end, alpha):
255 a = alpha_blend(start, control1, alpha)
256 b = alpha_blend(control1, control2, alpha)
257 c = alpha_blend(control2, end, alpha)
258 return alpha_blend(alpha_blend(a, b, alpha), alpha_blend(b, c, alpha),
259 alpha)
260
261
milind-u18a901d2023-02-17 21:51:55 -0800262SPLINE_SUBDIVISIONS = 100
263
264
265def subdivide_multistep():
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800266 # TODO: pick N based on spline parameters? or otherwise change it to be more evenly spaced?
milind-u18a901d2023-02-17 21:51:55 -0800267 for i in range(0, SPLINE_SUBDIVISIONS + 1):
268 yield i / float(SPLINE_SUBDIVISIONS)
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800269
270
milind-u18a901d2023-02-17 21:51:55 -0800271def get_proximal_distal_derivs(t_prev, t, t_next):
272 d_prev = normalize(t - t_prev)
273 d_next = normalize(t_next - t)
274 accel = (d_next - d_prev) / np.linalg.norm(t - t_next)
275 return (ThetaPoint(t[0], d_next[0],
276 accel[0]), ThetaPoint(t[1], d_next[1], accel[1]))
277
278
279def get_roll_joint_theta(theta_i, theta_f, t):
280 # Fit a theta(t) = (1 - cos(pi*t)) / 2,
281 # so that theta(0) = theta_i, and theta(1) = theta_f
282 offset = theta_i
283 scalar = (theta_f - theta_i) / 2.0
284 freq = np.pi
285 theta_curve = lambda t: scalar * (1 - np.cos(freq * t)) + offset
286
287 return theta_curve(t)
288
289
290def get_roll_joint_theta_multistep(alpha_rolls, alpha):
291 # Figure out which segment in the motion we're in
292 theta_i = None
293 theta_f = None
294 t = None
295
296 for i in range(len(alpha_rolls) - 1):
297 # Find the alpha segment we're in
298 if alpha_rolls[i][0] <= alpha <= alpha_rolls[i + 1][0]:
299 theta_i = alpha_rolls[i][1]
300 theta_f = alpha_rolls[i + 1][1]
301
302 total_dalpha = alpha_rolls[-1][0] - alpha_rolls[0][0]
303 assert total_dalpha == 1.0
304 dalpha = alpha_rolls[i + 1][0] - alpha_rolls[i][0]
305 t = (alpha - alpha_rolls[i][0]) * (total_dalpha / dalpha)
306 break
307 assert theta_i is not None
308 assert theta_f is not None
309 assert t is not None
310
311 return get_roll_joint_theta(theta_i, theta_f, t)
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800312
313
Maxwell Henderson83cf6d62023-02-10 20:29:26 -0800314# Draw a list of lines to a cairo context.
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800315def draw_lines(cr, lines):
316 cr.move_to(lines[0][0], lines[0][1])
317 for pt in lines[1:]:
318 cr.line_to(pt[0], pt[1])
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800319
320
milind-u18a901d2023-02-17 21:51:55 -0800321class Path(abc.ABC):
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800322
milind-u18a901d2023-02-17 21:51:55 -0800323 def __init__(self, name):
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800324 self.name = name
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800325
milind-u18a901d2023-02-17 21:51:55 -0800326 @abc.abstractmethod
327 def DoToThetaPoints(self):
328 pass
329
330 @abc.abstractmethod
331 def DoDrawTo(self):
332 pass
333
334 @abc.abstractmethod
335 def roll_joint_thetas(self):
336 pass
337
338 @abc.abstractmethod
339 def intersection(self, event):
340 pass
341
342 def roll_joint_collision(self, points, verbose=False):
343 for point in points:
344 if roll_joint_collision(*point):
345 if verbose:
346 print("Roll joint collision for path %s in point %s" %
347 (self.name, point))
348 return True
349 return False
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800350
351 def DrawTo(self, cr, theta_version):
milind-u18a901d2023-02-17 21:51:55 -0800352 if self.roll_joint_collision(self.DoToThetaPoints()):
353 cr.set_source_rgb(1.0, 0.0, 0.0)
354 self.DoDrawTo(cr, theta_version)
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800355
356 def ToThetaPoints(self):
milind-u18a901d2023-02-17 21:51:55 -0800357 points = self.DoToThetaPoints()
358 if self.roll_joint_collision(points, verbose=True):
359 sys.exit(1)
360 return points
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800361
362
milind-u18a901d2023-02-17 21:51:55 -0800363class SplineSegmentBase(Path):
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800364
milind-u18a901d2023-02-17 21:51:55 -0800365 def __init__(self, name):
366 super().__init__(name)
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800367
milind-u18a901d2023-02-17 21:51:55 -0800368 @abc.abstractmethod
369 # Returns (start, control1, control2, end), each in the form
370 # (theta1, theta2, theta3)
371 def get_controls_theta(self):
372 pass
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800373
milind-u18a901d2023-02-17 21:51:55 -0800374 def intersection(self, event):
375 start, control1, control2, end = self.get_controls_theta()
376 for alpha in subdivide_multistep():
377 x, y = get_xy(spline_eval(start, control1, control2, end, alpha))
378 spline_point = np.array([x, y])
379 hovered_point = np.array([event.x, event.y])
380 if np.linalg.norm(hovered_point - spline_point) < 0.03:
381 return alpha
382 return None
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800383
384
milind-u18a901d2023-02-17 21:51:55 -0800385class ThetaSplineSegment(SplineSegmentBase):
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800386
milind-u18a901d2023-02-17 21:51:55 -0800387 # start and end are [theta1, theta2, theta3].
388 # controls are just [theta1, theta2].
389 # control_alpha_rolls are a list of [alpha, roll]
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800390 def __init__(self,
milind-u18a901d2023-02-17 21:51:55 -0800391 name,
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800392 start,
393 control1,
394 control2,
395 end,
milind-u18a901d2023-02-17 21:51:55 -0800396 control_alpha_rolls=[],
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800397 alpha_unitizer=None,
398 vmax=None):
milind-u18a901d2023-02-17 21:51:55 -0800399 super().__init__(name)
400 self.start = start[:2]
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800401 self.control1 = control1
402 self.control2 = control2
milind-u18a901d2023-02-17 21:51:55 -0800403 self.end = end[:2]
404 # There will always be roll at alpha = 0 and 1
405 self.alpha_rolls = [[0.0, start[2]]
406 ] + control_alpha_rolls + [[1.0, end[2]]]
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800407 self.alpha_unitizer = alpha_unitizer
408 self.vmax = vmax
409
410 def __repr__(self):
milind-u18a901d2023-02-17 21:51:55 -0800411 return "ThetaSplineSegment(%s, %s, %s, %s)" % (repr(
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800412 self.start), repr(self.control1), repr(
413 self.control2), repr(self.end))
414
milind-u18a901d2023-02-17 21:51:55 -0800415 def DoDrawTo(self, cr, theta_version):
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800416 if (theta_version):
417 draw_lines(cr, [
418 spline_eval(self.start, self.control1, self.control2, self.end,
milind-u18a901d2023-02-17 21:51:55 -0800419 alpha) for alpha in subdivide_multistep()
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800420 ])
421 else:
422 start = get_xy(self.start)
423 end = get_xy(self.end)
424
425 draw_lines(cr, [
426 get_xy(
427 spline_eval(self.start, self.control1, self.control2,
428 self.end, alpha))
milind-u18a901d2023-02-17 21:51:55 -0800429 for alpha in subdivide_multistep()
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800430 ])
431
432 cr.move_to(start[0] + xy_end_circle_size, start[1])
milind-u18a901d2023-02-17 21:51:55 -0800433 cr.arc(start[0], start[1], xy_end_circle_size, 0, 2.0 * np.pi)
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800434 cr.move_to(end[0] + xy_end_circle_size, end[1])
milind-u18a901d2023-02-17 21:51:55 -0800435 cr.arc(end[0], end[1], xy_end_circle_size, 0, 2.0 * np.pi)
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800436
milind-u18a901d2023-02-17 21:51:55 -0800437 def DoToThetaPoints(self):
438 points = []
439 for alpha in subdivide_multistep():
440 proximal, distal = spline_eval(self.start, self.control1,
441 self.control2, self.end, alpha)
442 roll_joint = get_roll_joint_theta_multistep(
443 self.alpha_rolls, alpha)
444 points.append((proximal, distal, roll_joint))
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800445
milind-u18a901d2023-02-17 21:51:55 -0800446 return points
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800447
milind-u18a901d2023-02-17 21:51:55 -0800448 def get_controls_theta(self):
449 return (self.start, self.control1, self.control2, self.end)
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -0800450
milind-u18a901d2023-02-17 21:51:55 -0800451 def roll_joint_thetas(self):
452 ts = []
453 thetas = []
454 for alpha in subdivide_multistep():
455 roll_joint = get_roll_joint_theta_multistep(
456 self.alpha_rolls, alpha)
457 thetas.append(roll_joint)
458 ts.append(alpha)
459 return ts, thetas