blob: 3ddfb370fe4d5e2ebfa993e1c58ae70299a1ba7d [file] [log] [blame]
Maxwell Hendersonf5123fe2023-02-04 13:44:41 -08001import numpy
2import cairo
3
4from frc971.control_loops.python.basic_window import OverrideMatrix, identity
5
6# joint_center in x-y space.
7joint_center = (-0.299, 0.299)
8
9# Joint distances (l1 = "proximal", l2 = "distal")
10l1 = 46.25 * 0.0254
11l2 = 43.75 * 0.0254
12
13max_dist = 0.01
14max_dist_theta = numpy.pi / 64
15xy_end_circle_size = 0.01
16theta_end_circle_size = 0.07
17
18
19def px(cr):
20 return OverrideMatrix(cr, identity)
21
22
23# Convert from x-y coordinates to theta coordinates.
24# orientation is a bool. This orientation is circular_index mod 2.
25# where circular_index is the circular index, or the position in the
26# "hyperextension" zones. "cross_point" allows shifting the place where
27# it rounds the result so that it draws nicer (no other functional differences).
28def to_theta(pt, circular_index, cross_point=-numpy.pi):
29 orient = (circular_index % 2) == 0
30 x = pt[0]
31 y = pt[1]
32 x -= joint_center[0]
33 y -= joint_center[1]
34 l3 = numpy.hypot(x, y)
35 t3 = numpy.arctan2(y, x)
36 theta1 = numpy.arccos((l1**2 + l3**2 - l2**2) / (2 * l1 * l3))
37
38 if orient:
39 theta1 = -theta1
40 theta1 += t3
41 theta1 = (theta1 - cross_point) % (2 * numpy.pi) + cross_point
42 theta2 = numpy.arctan2(y - l1 * numpy.sin(theta1),
43 x - l1 * numpy.cos(theta1))
44 return numpy.array((theta1, theta2))
45
46
47# Simple trig to go back from theta1, theta2 to x-y
48def to_xy(theta1, theta2):
49 x = numpy.cos(theta1) * l1 + numpy.cos(theta2) * l2 + joint_center[0]
50 y = numpy.sin(theta1) * l1 + numpy.sin(theta2) * l2 + joint_center[1]
51 orient = ((theta2 - theta1) % (2.0 * numpy.pi)) < numpy.pi
52 return (x, y, orient)
53
54
55def get_circular_index(theta):
56 return int(numpy.floor((theta[1] - theta[0]) / numpy.pi))
57
58
59def get_xy(theta):
60 theta1 = theta[0]
61 theta2 = theta[1]
62 x = numpy.cos(theta1) * l1 + numpy.cos(theta2) * l2 + joint_center[0]
63 y = numpy.sin(theta1) * l1 + numpy.sin(theta2) * l2 + joint_center[1]
64 return numpy.array((x, y))
65
66
67# Subdivide in theta space.
68def subdivide_theta(lines):
69 out = []
70 last_pt = lines[0]
71 out.append(last_pt)
72 for n_pt in lines[1:]:
73 for pt in subdivide(last_pt, n_pt, max_dist_theta):
74 out.append(pt)
75 last_pt = n_pt
76
77 return out
78
79
80# subdivide in xy space.
81def subdivide_xy(lines, max_dist=max_dist):
82 out = []
83 last_pt = lines[0]
84 out.append(last_pt)
85 for n_pt in lines[1:]:
86 for pt in subdivide(last_pt, n_pt, max_dist):
87 out.append(pt)
88 last_pt = n_pt
89
90 return out
91
92
93def to_theta_with_ci(pt, circular_index):
94 return to_theta_with_circular_index(pt[0], pt[1], circular_index)
95
96
97# to_theta, but distinguishes between
98def to_theta_with_circular_index(x, y, circular_index):
99 theta1, theta2 = to_theta((x, y), circular_index)
100 n_circular_index = int(numpy.floor((theta2 - theta1) / numpy.pi))
101 theta2 = theta2 + ((circular_index - n_circular_index)) * numpy.pi
102 return numpy.array((theta1, theta2))
103
104
105# alpha is in [0, 1] and is the weight to merge a and b.
106def alpha_blend(a, b, alpha):
107 """Blends a and b.
108
109 Args:
110 alpha: double, Ratio. Needs to be in [0, 1] and is the weight to blend a
111 and b.
112 """
113 return b * alpha + (1.0 - alpha) * a
114
115
116def normalize(v):
117 """Normalize a vector while handling 0 length vectors."""
118 norm = numpy.linalg.norm(v)
119 if norm == 0:
120 return v
121 return v / norm
122
123
124# CI is circular index and allows selecting between all the stats that map
125# to the same x-y state (by giving them an integer index).
126# This will compute approximate first and second derivatives with respect
127# to path length.
128def to_theta_with_circular_index_and_derivs(x, y, dx, dy,
129 circular_index_select):
130 a = to_theta_with_circular_index(x, y, circular_index_select)
131 b = to_theta_with_circular_index(x + dx * 0.0001, y + dy * 0.0001,
132 circular_index_select)
133 c = to_theta_with_circular_index(x - dx * 0.0001, y - dy * 0.0001,
134 circular_index_select)
135 d1 = normalize(b - a)
136 d2 = normalize(c - a)
137 accel = (d1 + d2) / numpy.linalg.norm(a - b)
138 return (a[0], a[1], d1[0], d1[1], accel[0], accel[1])
139
140
141def to_theta_with_ci_and_derivs(p_prev, p, p_next, c_i_select):
142 a = to_theta(p, c_i_select)
143 b = to_theta(p_next, c_i_select)
144 c = to_theta(p_prev, c_i_select)
145 d1 = normalize(b - a)
146 d2 = normalize(c - a)
147 accel = (d1 + d2) / numpy.linalg.norm(a - b)
148 return (a[0], a[1], d1[0], d1[1], accel[0], accel[1])
149
150
151# Generic subdivision algorithm.
152def subdivide(p1, p2, max_dist):
153 dx = p2[0] - p1[0]
154 dy = p2[1] - p1[1]
155 dist = numpy.sqrt(dx**2 + dy**2)
156 n = int(numpy.ceil(dist / max_dist))
157 return [(alpha_blend(p1[0], p2[0],
158 float(i) / n), alpha_blend(p1[1], p2[1],
159 float(i) / n))
160 for i in range(1, n + 1)]
161
162
163# convert from an xy space loop into a theta loop.
164# All segements are expected go from one "hyper-extension" boundary
165# to another, thus we must go backwards over the "loop" to get a loop in
166# x-y space.
167def to_theta_loop(lines, cross_point=-numpy.pi):
168 out = []
169 last_pt = lines[0]
170 for n_pt in lines[1:]:
171 for pt in subdivide(last_pt, n_pt, max_dist):
172 out.append(to_theta(pt, 0, cross_point))
173 last_pt = n_pt
174 for n_pt in reversed(lines[:-1]):
175 for pt in subdivide(last_pt, n_pt, max_dist):
176 out.append(to_theta(pt, 1, cross_point))
177 last_pt = n_pt
178 return out
179
180
181# Convert a loop (list of line segments) into
182# The name incorrectly suggests that it is cyclic.
183def back_to_xy_loop(lines):
184 out = []
185 last_pt = lines[0]
186 out.append(to_xy(last_pt[0], last_pt[1]))
187 for n_pt in lines[1:]:
188 for pt in subdivide(last_pt, n_pt, max_dist_theta):
189 out.append(to_xy(pt[0], pt[1]))
190 last_pt = n_pt
191
192 return out
193
194
195def spline_eval(start, control1, control2, end, alpha):
196 a = alpha_blend(start, control1, alpha)
197 b = alpha_blend(control1, control2, alpha)
198 c = alpha_blend(control2, end, alpha)
199 return alpha_blend(alpha_blend(a, b, alpha), alpha_blend(b, c, alpha),
200 alpha)
201
202
203def subdivide_spline(start, control1, control2, end):
204 # TODO: pick N based on spline parameters? or otherwise change it to be more evenly spaced?
205 n = 100
206 for i in range(0, n + 1):
207 yield i / float(n)
208
209
210def get_derivs(t_prev, t, t_next):
211 c, a, b = t_prev, t, t_next
212 d1 = normalize(b - a)
213 d2 = normalize(c - a)
214 accel = (d1 + d2) / numpy.linalg.norm(a - b)
215 return (a[0], a[1], d1[0], d1[1], accel[0], accel[1])
216
217
218# Draw lines to cr + stroke.
219def draw_lines(cr, lines):
220 cr.move_to(lines[0][0], lines[0][1])
221 for pt in lines[1:]:
222 cr.line_to(pt[0], pt[1])
223 with px(cr):
224 cr.stroke()
225
226
227# Segment in angle space.
228class AngleSegment:
229
230 def __init__(self, start, end, name=None, alpha_unitizer=None, vmax=None):
231 """Creates an angle segment.
232
233 Args:
234 start: (double, double), The start of the segment in theta1, theta2
235 coordinates in radians
236 end: (double, double), The end of the segment in theta1, theta2
237 coordinates in radians
238 """
239 self.start = start
240 self.end = end
241 self.name = name
242 self.alpha_unitizer = alpha_unitizer
243 self.vmax = vmax
244
245 def __repr__(self):
246 return "AngleSegment(%s, %s)" % (repr(self.start), repr(self.end))
247
248 def DrawTo(self, cr, theta_version):
249 if theta_version:
250 cr.move_to(self.start[0], self.start[1] + theta_end_circle_size)
251 cr.arc(self.start[0], self.start[1], theta_end_circle_size, 0,
252 2.0 * numpy.pi)
253 cr.move_to(self.end[0], self.end[1] + theta_end_circle_size)
254 cr.arc(self.end[0], self.end[1], theta_end_circle_size, 0,
255 2.0 * numpy.pi)
256 cr.move_to(self.start[0], self.start[1])
257 cr.line_to(self.end[0], self.end[1])
258 else:
259 start_xy = to_xy(self.start[0], self.start[1])
260 end_xy = to_xy(self.end[0], self.end[1])
261 draw_lines(cr, back_to_xy_loop([self.start, self.end]))
262 cr.move_to(start_xy[0] + xy_end_circle_size, start_xy[1])
263 cr.arc(start_xy[0], start_xy[1], xy_end_circle_size, 0,
264 2.0 * numpy.pi)
265 cr.move_to(end_xy[0] + xy_end_circle_size, end_xy[1])
266 cr.arc(end_xy[0], end_xy[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
267
268 def ToThetaPoints(self):
269 dx = self.end[0] - self.start[0]
270 dy = self.end[1] - self.start[1]
271 mag = numpy.hypot(dx, dy)
272 dx /= mag
273 dy /= mag
274
275 return [(self.start[0], self.start[1], dx, dy, 0.0, 0.0),
276 (self.end[0], self.end[1], dx, dy, 0.0, 0.0)]
277
278
279class XYSegment:
280 """Straight line in XY space."""
281
282 def __init__(self, start, end, name=None, alpha_unitizer=None, vmax=None):
283 """Creates an XY segment.
284
285 Args:
286 start: (double, double), The start of the segment in theta1, theta2
287 coordinates in radians
288 end: (double, double), The end of the segment in theta1, theta2
289 coordinates in radians
290 """
291 self.start = start
292 self.end = end
293 self.name = name
294 self.alpha_unitizer = alpha_unitizer
295 self.vmax = vmax
296
297 def __repr__(self):
298 return "XYSegment(%s, %s)" % (repr(self.start), repr(self.end))
299
300 def DrawTo(self, cr, theta_version):
301 if theta_version:
302 theta1, theta2 = self.start
303 circular_index_select = int(
304 numpy.floor((self.start[1] - self.start[0]) / numpy.pi))
305 start = get_xy(self.start)
306 end = get_xy(self.end)
307
308 ln = [(start[0], start[1]), (end[0], end[1])]
309 draw_lines(cr, [
310 to_theta_with_circular_index(x, y, circular_index_select)
311 for x, y in subdivide_xy(ln)
312 ])
313 cr.move_to(self.start[0] + theta_end_circle_size, self.start[1])
314 cr.arc(self.start[0], self.start[1], theta_end_circle_size, 0,
315 2.0 * numpy.pi)
316 cr.move_to(self.end[0] + theta_end_circle_size, self.end[1])
317 cr.arc(self.end[0], self.end[1], theta_end_circle_size, 0,
318 2.0 * numpy.pi)
319 else:
320 start = get_xy(self.start)
321 end = get_xy(self.end)
322 cr.move_to(start[0], start[1])
323 cr.line_to(end[0], end[1])
324 cr.move_to(start[0] + xy_end_circle_size, start[1])
325 cr.arc(start[0], start[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
326 cr.move_to(end[0] + xy_end_circle_size, end[1])
327 cr.arc(end[0], end[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
328
329 def ToThetaPoints(self):
330 """ Converts to points in theta space via to_theta_with_circular_index_and_derivs"""
331 theta1, theta2 = self.start
332 circular_index_select = int(
333 numpy.floor((self.start[1] - self.start[0]) / numpy.pi))
334 start = get_xy(self.start)
335 end = get_xy(self.end)
336
337 ln = [(start[0], start[1]), (end[0], end[1])]
338
339 dx = end[0] - start[0]
340 dy = end[1] - start[1]
341 mag = numpy.hypot(dx, dy)
342 dx /= mag
343 dy /= mag
344
345 return [
346 to_theta_with_circular_index_and_derivs(x, y, dx, dy,
347 circular_index_select)
348 for x, y in subdivide_xy(ln, 0.01)
349 ]
350
351
352class SplineSegment:
353
354 def __init__(self,
355 start,
356 control1,
357 control2,
358 end,
359 name=None,
360 alpha_unitizer=None,
361 vmax=None):
362 self.start = start
363 self.control1 = control1
364 self.control2 = control2
365 self.end = end
366 self.name = name
367 self.alpha_unitizer = alpha_unitizer
368 self.vmax = vmax
369
370 def __repr__(self):
371 return "SplineSegment(%s, %s, %s, %s)" % (repr(
372 self.start), repr(self.control1), repr(
373 self.control2), repr(self.end))
374
375 def DrawTo(self, cr, theta_version):
376 if theta_version:
377 c_i_select = get_circular_index(self.start)
378 start = get_xy(self.start)
379 control1 = get_xy(self.control1)
380 control2 = get_xy(self.control2)
381 end = get_xy(self.end)
382
383 draw_lines(cr, [
384 to_theta(spline_eval(start, control1, control2, end, alpha),
385 c_i_select)
386 for alpha in subdivide_spline(start, control1, control2, end)
387 ])
388 cr.move_to(self.start[0] + theta_end_circle_size, self.start[1])
389 cr.arc(self.start[0], self.start[1], theta_end_circle_size, 0,
390 2.0 * numpy.pi)
391 cr.move_to(self.end[0] + theta_end_circle_size, self.end[1])
392 cr.arc(self.end[0], self.end[1], theta_end_circle_size, 0,
393 2.0 * numpy.pi)
394 else:
395 start = get_xy(self.start)
396 control1 = get_xy(self.control1)
397 control2 = get_xy(self.control2)
398 end = get_xy(self.end)
399
400 draw_lines(cr, [
401 spline_eval(start, control1, control2, end, alpha)
402 for alpha in subdivide_spline(start, control1, control2, end)
403 ])
404
405 cr.move_to(start[0] + xy_end_circle_size, start[1])
406 cr.arc(start[0], start[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
407 cr.move_to(end[0] + xy_end_circle_size, end[1])
408 cr.arc(end[0], end[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
409
410 def ToThetaPoints(self):
411 t1, t2 = self.start
412 c_i_select = get_circular_index(self.start)
413 start = get_xy(self.start)
414 control1 = get_xy(self.control1)
415 control2 = get_xy(self.control2)
416 end = get_xy(self.end)
417
418 return [
419 to_theta_with_ci_and_derivs(
420 spline_eval(start, control1, control2, end, alpha - 0.00001),
421 spline_eval(start, control1, control2, end, alpha),
422 spline_eval(start, control1, control2, end, alpha + 0.00001),
423 c_i_select)
424 for alpha in subdivide_spline(start, control1, control2, end)
425 ]
426
427
428class ThetaSplineSegment:
429
430 def __init__(self,
431 start,
432 control1,
433 control2,
434 end,
435 name=None,
436 alpha_unitizer=None,
437 vmax=None):
438 self.start = start
439 self.control1 = control1
440 self.control2 = control2
441 self.end = end
442 self.name = name
443 self.alpha_unitizer = alpha_unitizer
444 self.vmax = vmax
445
446 def __repr__(self):
447 return "ThetaSplineSegment(%s, %s, &s, %s)" % (repr(
448 self.start), repr(self.control1), repr(
449 self.control2), repr(self.end))
450
451 def DrawTo(self, cr, theta_version):
452 if (theta_version):
453 draw_lines(cr, [
454 spline_eval(self.start, self.control1, self.control2, self.end,
455 alpha)
456 for alpha in subdivide_spline(self.start, self.control1,
457 self.control2, self.end)
458 ])
459 else:
460 start = get_xy(self.start)
461 end = get_xy(self.end)
462
463 draw_lines(cr, [
464 get_xy(
465 spline_eval(self.start, self.control1, self.control2,
466 self.end, alpha))
467 for alpha in subdivide_spline(self.start, self.control1,
468 self.control2, self.end)
469 ])
470
471 cr.move_to(start[0] + xy_end_circle_size, start[1])
472 cr.arc(start[0], start[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
473 cr.move_to(end[0] + xy_end_circle_size, end[1])
474 cr.arc(end[0], end[1], xy_end_circle_size, 0, 2.0 * numpy.pi)
475
476 def ToThetaPoints(self):
477 return [
478 get_derivs(
479 spline_eval(self.start, self.control1, self.control2, self.end,
480 alpha - 0.00001),
481 spline_eval(self.start, self.control1, self.control2, self.end,
482 alpha),
483 spline_eval(self.start, self.control1, self.control2, self.end,
484 alpha + 0.00001))
485 for alpha in subdivide_spline(self.start, self.control1,
486 self.control2, self.end)
487 ]
488
489
490def expand_points(points, max_distance):
491 """Expands a list of points to be at most max_distance apart
492
493 Generates the paths to connect the new points to the closest input points,
494 and the paths connecting the points.
495
496 Args:
497 points, list of tuple of point, name, The points to start with and fill
498 in.
499 max_distance, float, The max distance between two points when expanding
500 the graph.
501
502 Return:
503 points, edges
504 """
505 result_points = [points[0]]
506 result_paths = []
507 for point, name in points[1:]:
508 previous_point = result_points[-1][0]
509 previous_point_xy = get_xy(previous_point)
510 circular_index = get_circular_index(previous_point)
511
512 point_xy = get_xy(point)
513 norm = numpy.linalg.norm(point_xy - previous_point_xy)
514 num_points = int(numpy.ceil(norm / max_distance))
515 last_iteration_point = previous_point
516 for subindex in range(1, num_points):
517 subpoint = to_theta(alpha_blend(previous_point_xy, point_xy,
518 float(subindex) / num_points),
519 circular_index=circular_index)
520 result_points.append(
521 (subpoint, '%s%dof%d' % (name, subindex, num_points)))
522 result_paths.append(
523 XYSegment(last_iteration_point, subpoint, vmax=6.0))
524 if (last_iteration_point != previous_point).any():
525 result_paths.append(XYSegment(previous_point, subpoint))
526 if subindex == num_points - 1:
527 result_paths.append(XYSegment(subpoint, point, vmax=6.0))
528 else:
529 result_paths.append(XYSegment(subpoint, point))
530 last_iteration_point = subpoint
531 result_points.append((point, name))
532
533 return result_points, result_paths