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 DT = 0.02 # seconds, sampling period
16 WD = 0.5 # m, waypoint distance
20 return (0 < x) - (x < 0)
24 return ((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)**0.5
28 # see https://math.stackexchange.com/questions/1595872/arclength-between-two-points-on-a-circle-not-knowing-theta
31 return 2 * r * asin(d / 2 / r)
42 def count_to(seconds):
43 return abs(int(seconds / DT)) + 1
46 def normalize_path(path):
52 while path[0] == path[1]:
57 def check_limits(x, max_x):
66 def set_max(x, max_x):
75 def lines_intersect(li1, li2):
84 deno = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
87 t = (x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)
89 u = (x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)
92 if t < 0 or t > 1 or u < 0 or u > 1:
94 return x1 + t * (x2 - x1), y1 + t * (y2 - y1)
97 def point_on_right_side_of(p, li):
104 if sgn((x3 - x1) * (y2 - y1) - (y3 - y1) * (x2 - x1)) < 0:
110 def min_angle_between(p1, pp, p2):
115 dot = d1x * d2x + d1y * d2y
116 d1 = (d1x * d1x + d1y * d1y)**0.5
117 d2 = (d2x * d2x + d2y * d2y)**0.5
118 delta = acos(dot / (d1 * d2))
119 return min(delta, pi - delta)
122 def find_segments_of(path):
124 last_segment = PathSegment(0, path[0])
125 for i in range(len(path)):
126 if (path[i][4] != last_segment.type()
127 or sgn(path[i][3]) != last_segment.direction()):
128 last_segment.set_goal(i, path[i])
129 segments.append(last_segment)
130 last_segment = PathSegment(i, path[i])
131 last_segment.set_goal(len(path) - 1, path[-1])
132 segments.append(last_segment)
133 # Fix speed when the segment direction is 0.
134 for i in range(1, len(segments) - 1):
138 if cs.direction() == 0 and ls.direction() == ns.direction():
139 cs.init_pose[3] = ls.init_pose[3]
140 elif cs.direction() == 0:
141 cs.init_pose[3] = ns.init_pose[3]
145 def find_cusps_in(segments):
146 """Cusp pose is the first pose where the direction changed."""
148 for i in range(1, len(segments)):
149 if segments[i-1].direction() != segments[i].direction():
150 cusps.append(segments[i].init_index)
155 """Class describing path segment.
157 :param init_index: Initial index
158 :param init_pose: Initial pose
159 :param goal_index: Goal index
160 :param goal_pose: Goal pose
162 def __init__(self, ii, ip):
163 """Create new PathSegment object.
165 :param ii: Initial index
166 :param ip: Initial pose
168 if ip[4] not in [0, 1, -1]:
169 raise ValueError(f"Unrecognized PathSegment type '{ip[4]}'")
171 self.init_pose = list(ip)
172 self.goal_index = None
173 self.goal_pose = None
183 di = self.direction()
191 return f"[{self.init_index}, {self.goal_index}] {st} {di} {sl}"
193 def set_goal(self, gi, gp):
198 """Return segment type.
206 return self.init_pose[4]
209 """Return segment direction.
211 Segment direction can be:
217 return sgn(self.init_pose[3])
221 return ed(self.goal_pose, self.init_pose)
223 return arc_len(self.init_pose, self.goal_pose)
225 def pose_at(self, distance):
226 """Return pose at specific distance.
228 Well, this does not work well.
232 self.goal_pose[1] - self.init_pose[1],
233 self.goal_pose[0] - self.init_pose[0])
235 self.init_pose[0] + distance * cos(h),
236 self.init_pose[1] + distance * sin(h),
239 x = self.init_pose[0]
240 y = self.init_pose[1]
241 h = self.init_pose[2]
243 self.direction() * distance / BicycleCar().wb
244 * tan(deg_to_rad(36) * self.type()))
245 x += self.direction() * distance * cos(h)
246 y += self.direction() * distance * sin(h)
247 return [x, y, h, h, None, None]
251 ctc = 11.2531 # this is guess resulting to max_wa ~= 35.99998076387121
255 df = 2.895 + 0.9 # this is guess
256 dr = 4.918 - df # length - df
258 max_sp = 3 / 3.6 # mps
261 mtr = ((ctc / 2)**2 - wb**2)**0.5 - ft/2
262 max_wa = atan(wb / mtr) # rad
263 max_wa_rate = deg_to_rad(1) # rad per sample
265 def __init__(self, x=0, y=0, h=0):
266 self.x = x # x, y is center of rear axle
270 self.wa = 0 # wheel angle
271 self.dwa = 0 # desired wa
276 return [self.x, self.y, self.h, self.wa, self.sp, self.acc]
279 """Euclidean distance from self to pose in 2D."""
280 return ed(self.pose(), pose)
283 """Return time to speed.
285 It is expected that speed is reachable from the current state,
286 i.e. speed 0 is unreachable from speed 3/3.6 when acc is greater
289 sp = check_limits(sp, self.max_sp)
290 t = (sp - self.sp) / self.acc
294 def bd(self, t=None):
295 """Return braking distance.
297 :param t: Time to stop (speed = 0).
299 See https://en.wikipedia.org/wiki/Braking_distance
300 See https://en.wikipedia.org/wiki/Friction#Coefficient_of_friction
301 See https://en.wikipedia.org/wiki/Standard_gravity
303 But see https://www.nabla.cz/obsah/fyzika/mechanika/rovnomerne-zrychleny-primocary-pohyb.php
305 s = v_0 * t + 1/2 * a * t**2
308 return self.sp * t + 1/2 * self.acc * t**2
310 # The following is a theoretical braking distance.
313 # return self.sp**2 / 2 / mu / g
315 # This is braking distance we need:
316 t = abs(self.sp / self.acc)
317 return self.sp * t + 1/2 * self.acc * t**2
320 """Return time to wheel angle."""
321 wa = check_limits(wa, self.max_wa)
322 return (wa - self.wa) / self.max_wa_rate * DT
324 def poses_to(self, pose):
325 """Return tuple of poses toward pose.
327 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)
328 see https://math.stackexchange.com/questions/285866/calculating-circle-radius-from-two-points-on-circumference-for-game-movement
329 see https://en.wikipedia.org/wiki/Arc_length
335 while self.sp != 0 and d < s:
338 poses.append(self.pose())
342 def set_wa_based_on_dwa(self):
343 if abs(self.dwa - self.wa) > self.max_wa_rate:
344 if self.dwa > self.wa:
345 self.wa += self.max_wa_rate
347 self.wa -= self.max_wa_rate
352 self.set_wa_based_on_dwa()
353 self.sp += self.acc * DT
354 self.sp = check_limits(self.sp, self.max_sp)
356 if self.acc < 0 and self.sp < 0:
359 elif self.acc > 0 and self.sp > 0:
362 self.x += DT * self.sp * cos(self.h)
363 self.y += DT * self.sp * sin(self.h)
364 self.h += DT * self.sp / self.wb * tan(self.wa)
370 def brake_within(self, d):
372 self.acc = check_limits(-1 * bd * self.sp**2 / 2 / d, self.max_acc)
375 def wa_to(self, pose):
378 self.wa = self.max_wa
379 # s = self.mtr * asin(self.ed(pose) / 2 / self.mtr)
381 self.wa = -self.max_wa
382 # s = self.mtr * asin(self.ed(pose) / 2 / self.mtr)
387 def find_cusps(path):
390 for i in range(1, len(path) - 1):
392 if sgn(path[i-1][3]) != sgn(path[i+1][3]):
395 if sgn(path[i][3]) != sgn(path[i+1][3]) and path[i+1][3] != 0:
400 if __name__ == "__main__":
402 with open(f"orig-{PLAN_FILE}.json", "r") as f:
404 plan["nnpath"] = list(plan["path"])
405 plan["path"] = normalize_path(plan["path"])
406 path = list(plan["path"])
407 cusps = find_cusps(path)
408 path[0][3] = path[1][3] # initial point is part of the first segment
409 path[0][4] = path[1][4]
410 path[-1][3] = path[-2][3] # goal point is part of the last segment
411 path[-1][4] = path[-2][4]
412 path_segments = find_segments_of(path)
413 cusps = find_cusps_in(path_segments)
414 print(f"path segments ({len(path_segments)}):")
415 for s in path_segments:
417 print(f"cusps ({len(cusps)}):")
418 for i in range(len(cusps)):
419 print(f"- {cusps[i]}")
420 c = BicycleCar(*path[0][:3])
421 c.acc = c.max_acc # start move forward
430 d += ed(path[j], path[j + 1])
432 if len(path) == 1: # last move
433 lpose = list(plan["ispath"][-1])
435 if PLAN_FILE.startswith("pe"):
439 c.brake_within(c.ed(lpose))
440 traj += c.poses_to(lpose)
446 traj.append(c.pose())
450 traj += c.poses_to(path[0])
453 while abs(c.sp - 0) > 1e-3:
455 traj.append(c.pose())
461 traj.append(c.pose())
462 c.acc = sgn(path[1][3]) * c.max_acc
465 traj.append(c.pose())
468 print(f"left {len(path)} waypoints")
470 with open(f"{PLAN_FILE}.json", "w") as f:
471 json.dump(plan, f, indent="\t")
472 with open(f"{PLAN_FILE}.h", "w") as f:
473 f.write("#ifndef COMMANDER_COMMANDS_H\n")
474 f.write("#define COMMANDER_COMMANDS_H\n")
475 f.write(f"#define COMMANDS_COUNT {len(traj)}\n")
476 f.write("struct command { double wa; double sp; double acc; };\n")
477 f.write("struct command commands[] = {\n")
479 f.write(f" {{{rad_to_deg(p[3])}, {p[4]}, {p[5]}}},\n")
481 f.write("#endif // COMMANDER_COMMANDS_H\n")