|
| 1 | +from math import sqrt |
| 2 | +import random |
| 3 | +import math |
| 4 | + |
| 5 | +import matplotlib.pyplot as plt |
| 6 | + |
| 7 | +# PATH = [(-36, 12), (-12,-12), (12, 12), (36,-12)] |
| 8 | +PATH = [(-36, 48), (-24,36), (-12, 0), (-12,-52), (0, -52), (0, 0), (12, 0), (12, 36), (24, 48), (36, 48)] |
| 9 | +START_POSE = (-36, 48, 0) |
| 10 | + |
| 11 | +# Simulation Constants |
| 12 | +LOOPTIME = 0.02 # seconds |
| 13 | +LOOPTIME_DEVIATION = 0.0 # seconds |
| 14 | + |
| 15 | +MAX_VELOCITY = 62 # your unit per second |
| 16 | +MAX_ACCELERATION = 25 # your unit per second squared |
| 17 | +MAX_ANGULAR_VELOCITY = 1.0 # rad/s |
| 18 | +MAX_ANGULAR_ACCELERATION = 1.0 # rad/s^2 |
| 19 | +CRUISE_VELOCITY = 30 # your unit per second |
| 20 | +ONE_FRAME_ACCEL = MAX_ACCELERATION * LOOPTIME |
| 21 | + |
| 22 | +PRECISION_RADIUS = 3 # your unit |
| 23 | +SLOW_DOWN_RADIUS = CRUISE_VELOCITY / ONE_FRAME_ACCEL # your unit |
| 24 | +LOOKAHEAD = 0.0 # WARNING: BETA. YMMV |
| 25 | + |
| 26 | +SLOW_DOWN_TURN = True |
| 27 | + |
| 28 | +USE_SPLINE = False |
| 29 | +SPLINE_RESOLUTION = 20 |
| 30 | + |
| 31 | +ANIMATE = True |
| 32 | + |
| 33 | +SHOW_ACCEL_VELO_GRAPH = False |
| 34 | +SHOW_GRID = True |
| 35 | +GRID_INCREMENT = 24 # your unit |
| 36 | + |
| 37 | +FIELD_IMAGE = "./centerstage.png" |
| 38 | +FIELD_WIDTH = 144 # your unit |
| 39 | +FIELD_LENGTH = 144 # your unit |
| 40 | + |
| 41 | +class Pose2d: |
| 42 | + def __init__(self, x, y, theta): |
| 43 | + self.x = x |
| 44 | + self.y = y |
| 45 | + self.theta = theta |
| 46 | + |
| 47 | + def distance(self, other): |
| 48 | + return ((self.x - other.x) ** 2 + ( |
| 49 | + self.y - other.y) ** 2) ** 0.5 |
| 50 | + |
| 51 | + def __sub__(self, other): |
| 52 | + return Pose2d(self.x - other.x, self.y - other.y, |
| 53 | + self.theta - other.theta) |
| 54 | + |
| 55 | + def __add__(self, other): |
| 56 | + return Pose2d(self.x + other.x, self.y + other.y, |
| 57 | + self.theta + other.theta) |
| 58 | + |
| 59 | + def __mul__(self, other): |
| 60 | + return Pose2d(self.x * other, self.y * other, |
| 61 | + self.theta * other) |
| 62 | + |
| 63 | + def __truediv__(self, other): |
| 64 | + return Pose2d(self.x / other, self.y / other, |
| 65 | + self.theta / other) |
| 66 | + |
| 67 | + def length(self): |
| 68 | + return (self.x ** 2 + self.y ** 2 + self.theta ** 2) ** 0.5 |
| 69 | + |
| 70 | + def __str__(self): |
| 71 | + return "x: " + str(self.x) + ", y: " + str( |
| 72 | + self.y) + ", theta: " + str(self.theta) |
| 73 | + def dot(self, other): # WARNING: BOTH POSE2DS ARE TREATES AS VECTORS WITH ORIGIN (0,0) |
| 74 | + return self.x * other.x + self.y * other.y |
| 75 | + |
| 76 | + def anglesimilarity(self, other): |
| 77 | + # need to get cosine squared of half the angle between the two vectors |
| 78 | + cos = (self.dot(other)/(self.length() * other.length())) |
| 79 | + return (1 + cos) / 2 |
| 80 | + |
| 81 | +def lerp(a, b, t): |
| 82 | + return a + (b - a) * t |
| 83 | + |
| 84 | +# https://stackoverflow.com/a/1084899/13224997 |
| 85 | +def circle_intersect_line(pose, x1, y1, x2, y2): |
| 86 | + d = (x2 - x1, y2 - y1) |
| 87 | + f = (x1 - pose.x, y1 - pose.y) |
| 88 | + |
| 89 | + a = d[0] ** 2 + d[1] ** 2 |
| 90 | + if a == 0: |
| 91 | + return False |
| 92 | + b = 2 * (f[0] * d[0] + f[1] * d[1]) |
| 93 | + c = (f[0] ** 2) + (f[1] ** 2) - (PRECISION_RADIUS ** 2) |
| 94 | + |
| 95 | + discriminant = (b ** 2) - (4 * a * c) |
| 96 | + if discriminant < 0: |
| 97 | + return False |
| 98 | + else: |
| 99 | + discriminant = discriminant ** 0.5 |
| 100 | + t1 = (-b - discriminant) / (2 * a) |
| 101 | + t2 = (-b + discriminant) / (2 * a) |
| 102 | + |
| 103 | + return (0 <= t1 <= 1 and 0 <= t2 <= 1) |
| 104 | + |
| 105 | + |
| 106 | +class Waypoint(Pose2d): |
| 107 | + def __init__(self, x, y, theta): |
| 108 | + super().__init__(x, y, theta) |
| 109 | + |
| 110 | + def is_hit(self, robotPose, oldRobotPose): |
| 111 | + if self.distance(robotPose) < PRECISION_RADIUS: |
| 112 | + return True |
| 113 | + elif (oldRobotPose.distance(robotPose) > self.distance( |
| 114 | + robotPose)): |
| 115 | + return circle_intersect_line(self, robotPose.x, robotPose.y, |
| 116 | + oldRobotPose.x, oldRobotPose.y) |
| 117 | + |
| 118 | + def __str__(self): |
| 119 | + return super().__str__() |
| 120 | + |
| 121 | + |
| 122 | +class RobotPose(Pose2d): |
| 123 | + def __init__(self, x, y, theta): |
| 124 | + super().__init__(x, y, theta) |
| 125 | + |
| 126 | + def __str__(self): |
| 127 | + return super().__str__() + ", velocity: "\ |
| 128 | + + ", angularVelocity: " |
| 129 | + |
| 130 | + |
| 131 | +if __name__ == "__main__": |
| 132 | + INITIAL_POSE = RobotPose(START_POSE[0], START_POSE[1], START_POSE[2]) |
| 133 | + ipoints = PATH |
| 134 | + splinex = [point[0] for point in ipoints] |
| 135 | + spliney = [point[1] for point in ipoints] |
| 136 | + import numpy as np |
| 137 | + |
| 138 | + half_width = FIELD_WIDTH/2 |
| 139 | + half_length = FIELD_LENGTH/2 |
| 140 | + |
| 141 | + plt.style.use('dark_background') |
| 142 | + if ANIMATE: |
| 143 | + h1 = plt.plot([], [], 'rx-') |
| 144 | + plt.xlim(-half_width, half_width) |
| 145 | + plt.ylim(-half_length, half_length) |
| 146 | + if SHOW_GRID: |
| 147 | + plt.grid(True) |
| 148 | + def update_line(hl, new_data): |
| 149 | + hl.set_xdata(np.append(hl.get_xdata(), new_data[0])) |
| 150 | + hl.set_ydata(np.append(hl.get_ydata(), new_data[1])) |
| 151 | + plt.draw() |
| 152 | + plt.pause(0.01) |
| 153 | + |
| 154 | + import cubicSpline |
| 155 | + if USE_SPLINE: |
| 156 | + splinex, spliney = cubicSpline.interpolate_xy(splinex, spliney, SPLINE_RESOLUTION) |
| 157 | + points = list(zip(splinex, spliney)) |
| 158 | + print(points) |
| 159 | + |
| 160 | + WAYPOINTS = [Waypoint(point[0], point[1], 0) for point in points] |
| 161 | + PreviousWaypoint = Waypoint(INITIAL_POSE.x, INITIAL_POSE.y, 0) |
| 162 | + robotPose = INITIAL_POSE |
| 163 | + robotPoseHistory = [robotPose] |
| 164 | + |
| 165 | + velocities = [] |
| 166 | + accelerations = [] |
| 167 | + |
| 168 | + def loop(): |
| 169 | + global LOOPTIME |
| 170 | + global PreviousWaypoint |
| 171 | + drift_looptime = LOOPTIME + random.uniform(-LOOPTIME_DEVIATION, |
| 172 | + LOOPTIME_DEVIATION) |
| 173 | + global robotPose |
| 174 | + global robotPoseHistory |
| 175 | + if WAYPOINTS[0].is_hit(robotPose, robotPoseHistory[-1]): |
| 176 | + PreviousWaypoint = WAYPOINTS[0] |
| 177 | + WAYPOINTS.pop(0) |
| 178 | + if len(WAYPOINTS) == 0: |
| 179 | + return False |
| 180 | + vel = robotPose - robotPoseHistory[-1] |
| 181 | + vel /= LOOPTIME |
| 182 | + |
| 183 | + robotPoseHistory.append(robotPose) |
| 184 | + |
| 185 | + vec_next_wpt = WAYPOINTS[0] - robotPose |
| 186 | + |
| 187 | + if len(WAYPOINTS) > 1: |
| 188 | + vec_second_wpt = WAYPOINTS[1] - WAYPOINTS[0] |
| 189 | + else: |
| 190 | + vec_second_wpt = vec_next_wpt |
| 191 | + |
| 192 | + t = 1 - (vec_next_wpt.length() / PreviousWaypoint.distance(WAYPOINTS[0])) |
| 193 | + v = 1 + (LOOKAHEAD * t * ((1-t) ** 4)) |
| 194 | + desiredvel = (vec_next_wpt * v) + (vec_second_wpt * (1-v)) |
| 195 | + |
| 196 | + if robotPose.distance(WAYPOINTS[-1]) >= SLOW_DOWN_RADIUS: |
| 197 | + desiredvel /= desiredvel.length() |
| 198 | + desiredvel *= CRUISE_VELOCITY |
| 199 | + |
| 200 | + else: |
| 201 | + desiredvel /= desiredvel.length() |
| 202 | + desiredvel *= lerp(CRUISE_VELOCITY, ONE_FRAME_ACCEL, 1-(robotPose.distance(WAYPOINTS[-1]) / (SLOW_DOWN_RADIUS-PRECISION_RADIUS))) |
| 203 | + |
| 204 | + # slow down on corners |
| 205 | + if (SLOW_DOWN_TURN and (len(WAYPOINTS) > 1)): |
| 206 | + if robotPose.distance(WAYPOINTS[0]) <= SLOW_DOWN_RADIUS: |
| 207 | + anglediff = vec_next_wpt.anglesimilarity(vec_second_wpt) |
| 208 | + desiredvel /= desiredvel.length() |
| 209 | + desiredvel *= lerp(CRUISE_VELOCITY, (anglediff*CRUISE_VELOCITY)+ONE_FRAME_ACCEL, 1-(robotPose.distance(WAYPOINTS[0]) / (SLOW_DOWN_RADIUS-PRECISION_RADIUS))) |
| 210 | + print(anglediff, 1-(robotPose.distance(WAYPOINTS[0]) / SLOW_DOWN_RADIUS)**2, desiredvel.length()) |
| 211 | + |
| 212 | + if desiredvel.length() > MAX_VELOCITY: |
| 213 | + desiredvel /= desiredvel.length() |
| 214 | + desiredvel *= MAX_VELOCITY |
| 215 | + |
| 216 | + accel = (desiredvel - vel) / LOOPTIME |
| 217 | + if accel.length() > MAX_ACCELERATION: |
| 218 | + accel /= accel.length() |
| 219 | + accel *= MAX_ACCELERATION |
| 220 | + |
| 221 | + accelerations.append(accel.length()) |
| 222 | + |
| 223 | + robotPose.velocity = vel + (accel * LOOPTIME) |
| 224 | + velocities.append(robotPose.velocity.length()) |
| 225 | + robotPose += robotPose.velocity * drift_looptime |
| 226 | + if ANIMATE: |
| 227 | + update_line(h1[0], (robotPose.x, robotPose.y)) |
| 228 | + return True |
| 229 | + |
| 230 | + import time |
| 231 | + start = time.perf_counter() |
| 232 | + while loop(): |
| 233 | + pass |
| 234 | + end = time.perf_counter() |
| 235 | + print("time", end - start, "s, average time", 1000 * (end - start) / len(robotPoseHistory), "ms") |
| 236 | + |
| 237 | + |
| 238 | + x = [] |
| 239 | + y = [] |
| 240 | + for pose in robotPoseHistory: |
| 241 | + x.append(pose.x) |
| 242 | + y.append(pose.y) |
| 243 | + |
| 244 | + print("simulated time: ", len(robotPoseHistory) * LOOPTIME, "s") |
| 245 | + |
| 246 | + total_dist = 0 |
| 247 | + for i in range(1, len(robotPoseHistory)): |
| 248 | + distance = sqrt((robotPoseHistory[i].x - robotPoseHistory[i-1].x)**2 + |
| 249 | + (robotPoseHistory[i].y - robotPoseHistory[i-1].y)**2) |
| 250 | + total_dist += distance |
| 251 | + |
| 252 | + print("Total distance: ", total_dist) |
| 253 | + |
| 254 | + if SHOW_ACCEL_VELO_GRAPH: |
| 255 | + times = [i * LOOPTIME for i in range(len(velocities))] |
| 256 | + |
| 257 | + plt.subplot(2, 1, 1) |
| 258 | + plt.plot(times, velocities, 'g-', label="Velocity", color="lime") |
| 259 | + plt.plot(times, accelerations, 'r-', label="Acceleration") |
| 260 | + plt.title("Acceleration and Velocity vs Time") |
| 261 | + plt.xlabel("Time (s)") |
| 262 | + plt.legend() |
| 263 | + plt.grid(SHOW_GRID) |
| 264 | + plt.subplot(2, 1, 2) |
| 265 | + |
| 266 | + plt.ylim(-half_length, half_length) |
| 267 | + plt.xlim(-half_width, half_width) |
| 268 | + plt.imshow(plt.imread("./centerstage.png"), extent=[-half_length, half_length, -half_width, half_width]) |
| 269 | + plt.plot([point[0] for point in ipoints], |
| 270 | + [point[1] for point in ipoints], 'bx-', |
| 271 | + color="fuchsia", label="Point to Point/Direct") |
| 272 | + plt.plot([point[0] for point in points], |
| 273 | + [point[1] for point in points], 'rx-', |
| 274 | + label="Spline") |
| 275 | + plt.plot(x, y, 'r-', color="lime", label="Loop Point/Predicted Localized Point") |
| 276 | + plt.title("Path") |
| 277 | + plt.legend() |
| 278 | + plt.ylabel("Y") |
| 279 | + plt.xlabel("X") |
| 280 | + plt.grid(SHOW_GRID, which="both", axis="both", linestyle="-", linewidth=0.5) |
| 281 | + plt.xticks(np.arange(-half_length, half_length + 1, GRID_INCREMENT)) |
| 282 | + plt.yticks(np.arange(-half_width, half_width + 1, GRID_INCREMENT)) |
| 283 | + |
| 284 | + plt.text(-half_width, half_length * 1.1, |
| 285 | + "Simulated time: " + str(round(len(robotPoseHistory) * LOOPTIME, 2)) + "s\nTotal distance: " + str(round(total_dist, 2)) + " units", |
| 286 | + horizontalalignment='center', |
| 287 | + verticalalignment='center', |
| 288 | + bbox=dict(facecolor='white', alpha=0.5) |
| 289 | + ) |
| 290 | + |
| 291 | + plt.pause(0.01) |
| 292 | + plt.show() |
0 commit comments