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
19 return (0 < x) - (x < 0)
23 return ((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)**0.5
34 def count_to(seconds):
35 return abs(int(seconds / DT)) + 1
38 def normalize_path(path):
44 while path[0] == path[1]:
49 def check_limits(x, max_x):
58 def set_max(x, max_x):
65 def lines_intersect(li1, li2):
74 deno = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
77 t = (x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)
79 u = (x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)
82 if t < 0 or t > 1 or u < 0 or u > 1:
84 return x1 + t * (x2 - x1), y1 + t * (y2 - y1)
87 def point_on_right_side_of(p, li):
94 if sgn((x3 - x1) * (y2 - y1) - (y3 - y1) * (x2 - x1)) < 0:
100 def min_angle_between(p1, pp, p2):
105 dot = d1x * d2x + d1y * d2y
106 d1 = (d1x * d1x + d1y * d1y)**0.5
107 d2 = (d2x * d2x + d2y * d2y)**0.5
108 delta = acos(dot / (d1 * d2))
109 return min(delta, pi - delta)
113 ctc = 11.2531 # this is guess resulting to max_wa ~= 35.99998076387121
117 df = 2.895 + 0.9 # this is guess
118 dr = 4.918 - df # length - df
120 max_sp = 3 / 3.6 # mps
123 mtr = ((ctc / 2)**2 - wb**2)**0.5 - ft/2
124 max_wa = atan(wb / mtr) # rad
125 max_wa_rate = deg_to_rad(1) # rad per sample
127 def __init__(self, x=0, y=0, h=0):
128 self.x = x # x, y is center of rear axle
132 self.wa = 0 # wheel angle
137 return [self.x, self.y, self.h, self.wa, self.sp, self.acc]
140 """Euclidean distance from self to pose in 2D."""
141 return ed(self.pose(), pose)
143 def bd(self, t=None):
144 """Return braking distance.
146 :param t: Time to stop (speed = 0).
148 See https://en.wikipedia.org/wiki/Braking_distance
149 See https://en.wikipedia.org/wiki/Friction#Coefficient_of_friction
150 See https://en.wikipedia.org/wiki/Standard_gravity
152 But see https://www.nabla.cz/obsah/fyzika/mechanika/rovnomerne-zrychleny-primocary-pohyb.php
154 s = v_0 * t + 1/2 * a * t**2
157 return self.sp * t + 1/2 * self.acc * t**2
159 # The following is a theoretical braking distance.
162 # return self.sp**2 / 2 / mu / g
164 # This is braking distance we need:
165 t = abs(self.sp / self.acc)
166 return self.sp * t + 1/2 * self.acc * t**2
169 def poses_to(self, pose):
170 """Return tuple of poses toward pose.
172 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)
173 see https://math.stackexchange.com/questions/285866/calculating-circle-radius-from-two-points-on-circumference-for-game-movement
174 see https://en.wikipedia.org/wiki/Arc_length
180 while self.sp != 0 and d < s:
183 poses.append(self.pose())
188 self.sp += self.acc * DT
189 self.sp = check_limits(self.sp, self.max_sp)
191 if self.acc < 0 and self.sp < 0:
194 elif self.acc > 0 and self.sp > 0:
197 self.h += self.sp / self.wb * tan(self.wa) * DT
202 self.x += DT * self.sp * cos(self.h)
203 self.y += DT * self.sp * sin(self.h)
205 def brake_within(self, d):
207 self.acc = check_limits(-1 * bd * self.sp**2 / 2 / d, self.max_acc)
210 def wa_to(self, pose):
213 self.wa = self.max_wa
214 # s = self.mtr * asin(self.ed(pose) / 2 / self.mtr)
216 self.wa = -self.max_wa
217 # s = self.mtr * asin(self.ed(pose) / 2 / self.mtr)
222 def find_cusps(path):
225 for i in range(1, len(path) - 1):
227 if sgn(path[i-1][3]) != sgn(path[i+1][3]):
230 if sgn(path[i][3]) != sgn(path[i+1][3]) and path[i+1][3] != 0:
235 if __name__ == "__main__":
237 with open(f"orig-{PLAN_FILE}.json", "r") as f:
239 plan["nnpath"] = list(plan["path"])
240 plan["path"] = normalize_path(plan["path"])
241 path = list(plan["path"])
242 cusps = find_cusps(path)
243 path[0][3] = path[1][3] # initial point is part of the first segment
244 path[0][4] = path[1][4]
245 path[-1][3] = path[-2][3] # goal point is part of the last segment
246 path[-1][4] = path[-2][4]
247 c = BicycleCar(*path[0][:3])
248 c.acc = c.max_acc # start move forward
257 d += ed(path[j], path[j + 1])
259 if len(path) == 1: # last move
260 lpose = list(plan["ispath"][-1])
262 if PLAN_FILE.startswith("pe"):
266 c.brake_within(c.ed(lpose))
267 traj += c.poses_to(lpose)
273 traj.append(c.pose())
277 traj += c.poses_to(path[0])
280 while abs(c.sp - 0) > 1e-3:
282 traj.append(c.pose())
288 traj.append(c.pose())
289 c.acc = sgn(path[1][3]) * c.max_acc
292 traj.append(c.pose())
295 print(f"left {len(path)} waypoints")
297 with open(f"{PLAN_FILE}.json", "w") as f:
298 json.dump(plan, f, indent="\t")
299 with open(f"{PLAN_FILE}.h", "w") as f:
300 f.write("#ifndef COMMANDER_COMMANDS_H\n")
301 f.write("#define COMMANDER_COMMANDS_H\n")
302 f.write(f"#define COMMANDS_COUNT {len(traj)}\n")
303 f.write("struct command { double wa; double sp; double acc; };\n")
304 f.write("struct command commands[] = {\n")
306 f.write(f" {{{rad_to_deg(p[3])}, {p[4]}, {p[5]}}},\n")
308 f.write("#endif // COMMANDER_COMMANDS_H\n")