2 """Translate equidistant path to equitemporal (isochronic) trajectory.
4 Path does not need to be equidistant, in fact. Typically, the last point
5 of the path segment is closer due to interpolation.
7 It is expected that the point of the path has five values: x, y,
8 heading, speed, and type, where type is +1 for left turn, -1 for right
9 turn, and 0 for straight segment.
11 from math import sin, atan, cos, tan, pi, acos, asin, atan2
15 PLAN_FILE = "orig-forward.json"
16 DT = 0.02 # seconds, sampling period
17 WD = 0.5 # m, waypoint distance
21 return (0 < x) - (x < 0)
25 return ((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)**0.5
29 # see https://math.stackexchange.com/questions/1595872/arclength-between-two-points-on-a-circle-not-knowing-theta
32 return 2 * r * asin(d / 2 / r)
43 def count_to(seconds):
44 return abs(int(seconds / DT)) + 1
47 def normalize_path(path):
53 while path[0] == path[1]:
58 def check_limits(x, max_x):
67 def set_max(x, max_x):
76 def lines_intersect(li1, li2):
85 deno = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
88 t = (x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)
90 u = (x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)
93 if t < 0 or t > 1 or u < 0 or u > 1:
95 return x1 + t * (x2 - x1), y1 + t * (y2 - y1)
98 def point_on_right_side_of(p, li):
105 if sgn((x3 - x1) * (y2 - y1) - (y3 - y1) * (x2 - x1)) < 0:
111 def min_angle_between(p1, pp, p2):
116 dot = d1x * d2x + d1y * d2y
117 d1 = (d1x * d1x + d1y * d1y)**0.5
118 d2 = (d2x * d2x + d2y * d2y)**0.5
119 delta = acos(dot / (d1 * d2))
120 return min(delta, pi - delta)
123 def find_segments_of(path):
125 last_segment = PathSegment(0, path[0])
126 for i in range(1, len(path) - 1):
127 if (path[i][4] != last_segment.type()
128 #or sgn(path[i][3]) != last_segment.direction()
130 last_segment.set_goal(i - 1, path[i - 1])
131 segments.append(last_segment)
132 last_segment = PathSegment(i - 1, path[i - 1])
133 last_segment.init_pose[3] = path[i][3]
134 last_segment.init_pose[4] = path[i][4]
135 last_segment.set_goal(len(path) - 1, path[-1])
136 segments.append(last_segment)
137 # Fix speed when the segment direction is 0.
138 for i in range(1, len(segments) - 1):
142 if cs.direction() == 0 and ls.direction() == ns.direction():
143 cs.init_pose[3] = ls.init_pose[3]
144 elif cs.direction() == 0:
145 cs.init_pose[3] = ns.init_pose[3]
149 def find_cusps_in(path):
150 """Cusp pose is the pose where the direction changed."""
152 for i in range(len(path)):
159 """Class describing path segment.
161 :param init_index: Initial index
162 :param init_pose: Initial pose
163 :param goal_index: Goal index
164 :param goal_pose: Goal pose
166 def __init__(self, ii, ip):
167 """Create new PathSegment object.
169 :param ii: Initial index
170 :param ip: Initial pose
172 if ip[4] not in [0, 1, -1]:
173 raise ValueError(f"Unrecognized PathSegment type '{ip[4]}'")
175 self.init_pose = list(ip)
176 self.goal_index = None
177 self.goal_pose = None
187 di = self.direction()
195 return f"[{self.init_index}, {self.goal_index}] {st} {di} {sl}"
197 def set_goal(self, gi, gp):
202 """Return segment type.
210 return self.init_pose[4]
213 """Return segment direction.
215 Segment direction can be:
221 return sgn(self.init_pose[3])
225 return ed(self.goal_pose, self.init_pose)
227 return arc_len(self.init_pose, self.goal_pose)
229 def pose_at(self, distance):
230 """Return pose at specific distance.
232 Well, this does not work well.
236 self.goal_pose[1] - self.init_pose[1],
237 self.goal_pose[0] - self.init_pose[0])
239 self.init_pose[0] + distance * cos(h),
240 self.init_pose[1] + distance * sin(h),
243 x = self.init_pose[0]
244 y = self.init_pose[1]
245 h = self.init_pose[2]
247 self.direction() * distance / BicycleCar().wb
248 * tan(deg_to_rad(36) * self.type()))
249 x += self.direction() * distance * cos(h)
250 y += self.direction() * distance * sin(h)
251 return [x, y, h, h, None, None]
255 ctc = 11.2531 # this is guess resulting to max_wa ~= 35.99998076387121
259 df = 2.895 + 0.9 # this is guess
260 dr = 4.918 - df # length - df
262 max_sp = 3 / 3.6 # mps
265 mtr = ((ctc / 2)**2 - wb**2)**0.5 - ft/2
266 max_wa = atan(wb / mtr) # rad
267 max_wa_rate = deg_to_rad(1) # rad per sample
269 def __init__(self, x=0, y=0, h=0):
270 self.x = x # x, y is center of rear axle
274 self.wa = 0 # wheel angle
275 self.dwa = 0 # desired wa
280 return [self.x, self.y, self.h, self.wa, self.sp, self.acc]
283 """Euclidean distance from self to pose in 2D."""
284 return ed(self.pose(), pose)
287 """Return time to speed.
289 It is expected that speed is reachable from the current state,
290 i.e. speed 0 is unreachable from speed 3/3.6 when acc is greater
293 sp = check_limits(sp, self.max_sp)
294 t = (sp - self.sp) / self.acc
298 def bd(self, t=None):
299 """Return braking distance.
301 :param t: Time to stop (speed = 0).
303 See https://en.wikipedia.org/wiki/Braking_distance
304 See https://en.wikipedia.org/wiki/Friction#Coefficient_of_friction
305 See https://en.wikipedia.org/wiki/Standard_gravity
307 But see https://www.nabla.cz/obsah/fyzika/mechanika/rovnomerne-zrychleny-primocary-pohyb.php
309 s = v_0 * t + 1/2 * a * t**2
312 return self.sp * t + 1/2 * self.acc * t**2
314 # The following is a theoretical braking distance.
317 # return self.sp**2 / 2 / mu / g
319 # This is braking distance we need:
320 t = abs(self.sp / self.acc)
321 return self.sp * t + 1/2 * self.acc * t**2
324 """Return time to wheel angle."""
325 wa = check_limits(wa, self.max_wa)
326 return (wa - self.wa) / self.max_wa_rate * DT
328 def poses_to(self, pose):
329 """Return tuple of poses toward pose.
331 see https://math.stackexchange.com/questions/1794943/how-to-calculate-the-distance-between-two-points-on-a-circle-in-degrees (error in answer, it's theta/2)
332 see https://math.stackexchange.com/questions/285866/calculating-circle-radius-from-two-points-on-circumference-for-game-movement
333 see https://en.wikipedia.org/wiki/Arc_length
339 while self.sp != 0 and d < s:
342 poses.append(self.pose())
346 def set_wa_based_on_dwa(self):
347 if abs(self.dwa - self.wa) > self.max_wa_rate:
348 if self.dwa > self.wa:
349 self.wa += self.max_wa_rate
351 self.wa -= self.max_wa_rate
356 self.set_wa_based_on_dwa()
357 self.sp += self.acc * DT
358 self.sp = check_limits(self.sp, self.max_sp)
360 if self.acc < 0 and self.sp < 0:
363 elif self.acc > 0 and self.sp > 0:
366 self.x += DT * self.sp * cos(self.h)
367 self.y += DT * self.sp * sin(self.h)
368 self.h += DT * self.sp / self.wb * tan(self.wa)
374 def brake_within(self, d):
375 """Brake within specified distance.
377 :param d: Distance to brake within.
379 The computation is based on:
381 1. time to stop = abs(speed / acc)
382 2. distance to stop = speed * t + 1/2 * acc * t**2
384 Now, substitute t in (2) by (1) and get acc from (2).
386 NOTE: I am not sure why 1/2 is better than 3/2. Probably, I
387 miscomputed something...
389 bd = sgn(self.sp) * -1
390 self.acc = check_limits(bd * 1/2 * self.sp**2 / d, self.max_acc)
393 def wa_to(self, pose):
396 self.wa = self.max_wa
397 # s = self.mtr * asin(self.ed(pose) / 2 / self.mtr)
399 self.wa = -self.max_wa
400 # s = self.mtr * asin(self.ed(pose) / 2 / self.mtr)
405 if __name__ == "__main__":
407 if len(sys.argv) == 2:
408 PLAN_FILE = sys.argv[1]
409 with open(f"{PLAN_FILE}", "r") as f:
411 plan["nnpath"] = list(plan["path"])
412 plan["path"] = normalize_path(plan["path"])
413 path = list(plan["path"])
414 cusps = find_cusps_in(path)
415 path_segments = find_segments_of(path)
416 print(f"path segments ({len(path_segments)}):")
417 for s in path_segments:
419 print(f"cusps ({len(cusps)}):")
420 for i in range(len(cusps)):
421 print(f"- {cusps[i]}")
422 c = BicycleCar(*path[0][:3])
424 # Set wa to the value of init pose.
425 if path_segments[0].type() == 1:
427 elif path_segments[0].type() == -1:
431 for i in range(count_to(c.ttwa(c.dwa))):
433 traj.append(c.pose())
435 # Interpolate over segments.
436 for i in range(len(path_segments)):
438 s_is_cusp = s.goal_index in cusps
439 c.acc = set_max(s.direction(), c.max_acc)
440 c.dwa = set_max(s.type(), c.max_wa)
443 if c.braking and sgn(c.sp) == sgn(c.acc):
446 and (max(1, s.len()/2) > abs(s.len() - d))
448 c.brake_within(abs(s.len() - d))
451 traj.append(c.pose())
455 d += arc_len(op, c.pose())
456 if c.braking and s_is_cusp:
459 for _ in range(count_to(3)):
461 traj.append(c.pose())
462 c.acc = set_max(path_segments[i+1].direction(), c.max_acc)
463 c.dwa = set_max(path_segments[i+1].type(), c.max_wa)
464 for _ in range(count_to(c.ttwa(c.dwa))):
466 traj.append(c.pose())
469 # Perform in slot maneuver.
470 assert sgn(c.sp) == sgn(c.acc)
471 lpose = list(plan["ispath"][-1])
474 if PLAN_FILE.startswith("pa"):
475 lpose.append(c.max_wa)
476 ll = arc_len(c.pose(), lpose)
485 if c.braking and sgn(c.sp) == sgn(c.acc):
489 traj.append(c.pose())
490 if PLAN_FILE.startswith("pa"):
491 d += arc_len(op, c.pose())
496 with open(f"{PLAN_FILE.split('.')[0]}.traj.json", "w") as f:
497 json.dump(plan, f, indent="\t")
498 with open(f"{PLAN_FILE.split('.')[0]}.traj.h", "w") as f:
499 f.write("#ifndef COMMANDER_COMMANDS_H\n")
500 f.write("#define COMMANDER_COMMANDS_H\n")
501 f.write(f"#define COMMANDS_COUNT {len(traj)}\n")
502 f.write("struct command { double wa; double sp; double acc; };\n")
503 f.write("struct command commands[] = {\n")
505 f.write(f" {{{rad_to_deg(p[3])}, {p[4]}, {p[5]}}},\n")
507 f.write("#endif // COMMANDER_COMMANDS_H\n")