diff --git a/.gitignore b/.gitignore index 28921ba1..9c4e9521 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ __pycache__/ *.py[cod] *$py.class +_build/ +.idea/ # C extensions *.so diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..79eaa29b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "doc/PythonRoboticsPaper"] + path = doc/PythonRoboticsPaper + url = https://github.com/AtsushiSakai/PythonRoboticsPaper + diff --git a/.travis.yml b/.travis.yml index d7fa6b3e..f1ff56a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ python: - 3.6 before_install: + - sudo apt-get update - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh - chmod +x miniconda.sh - bash miniconda.sh -b -p $HOME/miniconda @@ -14,13 +15,15 @@ before_install: - conda info -a install: - - pip install scipy - - pip install numpy - - pip install matplotlib - - pip install pandas - - pip install cvxpy - - conda install -y -c cvxgrp cvxpy - - conda install -y -c anaconda cvxopt + - conda install numpy + - conda install scipy + - conda install matplotlib + - conda install pandas + - conda install -c conda-forge lapack + - conda install -c cvxgrp cvxpy + #- pip install cvxpy + #- conda install -y -c cvxgrp cvxpy + #- conda install -y -c anaconda cvxopt script: - python --version diff --git a/AerialNavigation/Quadrotor.py b/AerialNavigation/drone_3d_trajectory_following/Quadrotor.py similarity index 56% rename from AerialNavigation/Quadrotor.py rename to AerialNavigation/drone_3d_trajectory_following/Quadrotor.py index db7965e8..de8308ff 100644 --- a/AerialNavigation/Quadrotor.py +++ b/AerialNavigation/drone_3d_trajectory_following/Quadrotor.py @@ -9,24 +9,26 @@ import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D + class Quadrotor(): - def __init__(self, x=0, y=0, z=0, roll=0, pitch=0, yaw=0, size=0.25): - self.p1 = np.array([size/2, 0, 0, 1]).T - self.p2 = np.array([-size/2, 0, 0, 1]).T - self.p3 = np.array([0, size/2, 0, 1]).T - self.p4 = np.array([0, -size/2, 0, 1]).T + def __init__(self, x=0, y=0, z=0, roll=0, pitch=0, yaw=0, size=0.25, show_animation=True): + self.p1 = np.array([size / 2, 0, 0, 1]).T + self.p2 = np.array([-size / 2, 0, 0, 1]).T + self.p3 = np.array([0, size / 2, 0, 1]).T + self.p4 = np.array([0, -size / 2, 0, 1]).T self.x_data = [] self.y_data = [] self.z_data = [] - - plt.ion() - - fig = plt.figure() - self.ax = fig.add_subplot(111, projection='3d') + self.show_animation = show_animation self.update_pose(x, y, z, roll, pitch, yaw) + if self.show_animation: + plt.ion() + fig = plt.figure() + self.ax = fig.add_subplot(111, projection='3d') + def update_pose(self, x, y, z, roll, pitch, yaw): self.x = x self.y = y @@ -37,7 +39,9 @@ class Quadrotor(): self.x_data.append(x) self.y_data.append(y) self.z_data.append(z) - self.plot() + + if self.show_animation: + self.plot() def transformation_matrix(self): x = self.x @@ -47,10 +51,11 @@ class Quadrotor(): pitch = self.pitch yaw = self.yaw return np.array( - [[cos(yaw)*cos(pitch), -sin(yaw)*cos(roll)+cos(yaw)*sin(pitch)*sin(roll), sin(yaw)*sin(roll)+cos(yaw)*sin(pitch)*cos(roll), x], - [sin(yaw)*cos(pitch), cos(yaw)*cos(roll)+sin(yaw)*sin(pitch)*sin(roll), -cos(yaw)*sin(roll)+sin(yaw)*sin(pitch)*cos(roll), y], - [-sin(pitch), cos(pitch)*sin(roll), cos(pitch)*cos(yaw), z] - ]) + [[cos(yaw) * cos(pitch), -sin(yaw) * cos(roll) + cos(yaw) * sin(pitch) * sin(roll), sin(yaw) * sin(roll) + cos(yaw) * sin(pitch) * cos(roll), x], + [sin(yaw) * cos(pitch), cos(yaw) * cos(roll) + sin(yaw) * sin(pitch) * + sin(roll), -cos(yaw) * sin(roll) + sin(yaw) * sin(pitch) * cos(roll), y], + [-sin(pitch), cos(pitch) * sin(roll), cos(pitch) * cos(yaw), z] + ]) def plot(self): T = self.transformation_matrix() @@ -66,8 +71,10 @@ class Quadrotor(): [p1_t[1], p2_t[1], p3_t[1], p4_t[1]], [p1_t[2], p2_t[2], p3_t[2], p4_t[2]], 'k.') - self.ax.plot([p1_t[0], p2_t[0]], [p1_t[1], p2_t[1]], [p1_t[2], p2_t[2]], 'r-') - self.ax.plot([p3_t[0], p4_t[0]], [p3_t[1], p4_t[1]], [p3_t[2], p4_t[2]], 'r-') + self.ax.plot([p1_t[0], p2_t[0]], [p1_t[1], p2_t[1]], + [p1_t[2], p2_t[2]], 'r-') + self.ax.plot([p3_t[0], p4_t[0]], [p3_t[1], p4_t[1]], + [p3_t[2], p4_t[2]], 'r-') self.ax.plot(self.x_data, self.y_data, self.z_data, 'b:') @@ -75,5 +82,4 @@ class Quadrotor(): plt.ylim(-5, 5) self.ax.set_zlim(0, 10) - plt.show() plt.pause(0.001) \ No newline at end of file diff --git a/AerialNavigation/TrajectoryGenerator.py b/AerialNavigation/drone_3d_trajectory_following/TrajectoryGenerator.py similarity index 100% rename from AerialNavigation/TrajectoryGenerator.py rename to AerialNavigation/drone_3d_trajectory_following/TrajectoryGenerator.py diff --git a/AerialNavigation/drone_3d_trajectory_following/animation.gif b/AerialNavigation/drone_3d_trajectory_following/animation.gif new file mode 100644 index 00000000..7474a4d3 Binary files /dev/null and b/AerialNavigation/drone_3d_trajectory_following/animation.gif differ diff --git a/AerialNavigation/quad_sim.py b/AerialNavigation/drone_3d_trajectory_following/drone_3d_trajectory_following.py similarity index 62% rename from AerialNavigation/quad_sim.py rename to AerialNavigation/drone_3d_trajectory_following/drone_3d_trajectory_following.py index 4de677c6..a4cdcbf2 100644 --- a/AerialNavigation/quad_sim.py +++ b/AerialNavigation/drone_3d_trajectory_following/drone_3d_trajectory_following.py @@ -9,7 +9,9 @@ import numpy as np from Quadrotor import Quadrotor from TrajectoryGenerator import TrajectoryGenerator -#Simulation parameters +show_animation = True + +# Simulation parameters g = 9.81 m = 0.2 Ixx = 1 @@ -17,7 +19,7 @@ Iyy = 1 Izz = 1 T = 5 -#Proportional coefficients +# Proportional coefficients Kp_x = 1 Kp_y = 1 Kp_z = 1 @@ -25,11 +27,12 @@ Kp_roll = 25 Kp_pitch = 25 Kp_yaw = 25 -#Derivative coefficients +# Derivative coefficients Kd_x = 10 Kd_y = 10 Kd_z = 1 + def quad_sim(x_c, y_c, z_c): """ Calculates the necessary thrust and torques for the quadrotor to @@ -57,9 +60,12 @@ def quad_sim(x_c, y_c, z_c): dt = 0.1 t = 0 - q = Quadrotor(x=x_pos, y=y_pos, z=z_pos, roll=roll, pitch=pitch, yaw=yaw, size=1) + q = Quadrotor(x=x_pos, y=y_pos, z=z_pos, roll=roll, + pitch=pitch, yaw=yaw, size=1, show_animation=show_animation) i = 0 + n_run = 8 + irun = 0 while True: while t <= T: @@ -73,38 +79,48 @@ def quad_sim(x_c, y_c, z_c): des_y_acc = calculate_acceleration(y_c[i], t) des_z_acc = calculate_acceleration(z_c[i], t) - thrust = m*(g + des_z_acc + Kp_z*(des_z_pos - z_pos) + Kd_z*(des_z_vel - z_vel)) + thrust = m * (g + des_z_acc + Kp_z * (des_z_pos - + z_pos) + Kd_z * (des_z_vel - z_vel)) - roll_torque = Kp_roll*(((des_x_acc*sin(des_yaw) - des_y_acc*cos(des_yaw))/g) - roll) - pitch_torque = Kp_pitch*(((des_x_acc*cos(des_yaw) - des_y_acc*sin(des_yaw))/g) - pitch) - yaw_torque = Kp_yaw*(des_yaw - yaw) + roll_torque = Kp_roll * \ + (((des_x_acc * sin(des_yaw) - des_y_acc * cos(des_yaw)) / g) - roll) + pitch_torque = Kp_pitch * \ + (((des_x_acc * cos(des_yaw) - des_y_acc * sin(des_yaw)) / g) - pitch) + yaw_torque = Kp_yaw * (des_yaw - yaw) - roll_vel += roll_torque*dt/Ixx - pitch_vel += pitch_torque*dt/Iyy - yaw_vel += yaw_torque*dt/Izz + roll_vel += roll_torque * dt / Ixx + pitch_vel += pitch_torque * dt / Iyy + yaw_vel += yaw_torque * dt / Izz - roll += roll_vel*dt - pitch += pitch_vel*dt - yaw += yaw_vel*dt + roll += roll_vel * dt + pitch += pitch_vel * dt + yaw += yaw_vel * dt R = rotation_matrix(roll, pitch, yaw) - acc = (np.matmul(R, np.array([0, 0, thrust]).T) - np.array([0, 0, m*g]).T)/m + acc = (np.matmul(R, np.array( + [0, 0, thrust]).T) - np.array([0, 0, m * g]).T) / m x_acc = acc[0] y_acc = acc[1] z_acc = acc[2] - x_vel += x_acc*dt - y_vel += y_acc*dt - z_vel += z_acc*dt - x_pos += x_vel*dt - y_pos += y_vel*dt - z_pos += z_vel*dt + x_vel += x_acc * dt + y_vel += y_acc * dt + z_vel += z_acc * dt + x_pos += x_vel * dt + y_pos += y_vel * dt + z_pos += z_vel * dt q.update_pose(x_pos, y_pos, z_pos, roll, pitch, yaw) t += dt t = 0 - i = (i+1)%4 + i = (i + 1) % 4 + irun += 1 + if irun >= n_run: + break + + print("Done") + def calculate_position(c, t): """ @@ -118,7 +134,8 @@ def calculate_position(c, t): Returns Position """ - return c[0]*t**5 + c[1]*t**4 + c[2]*t**3 + c[3]*t**2 + c[4]*t + c[5] + return c[0] * t**5 + c[1] * t**4 + c[2] * t**3 + c[3] * t**2 + c[4] * t + c[5] + def calculate_velocity(c, t): """ @@ -132,7 +149,8 @@ def calculate_velocity(c, t): Returns Velocity """ - return 5*c[0]*t**4 + 4*c[1]*t**3 + 3*c[2]*t**2 + 2*c[3]*t + c[4] + return 5 * c[0] * t**4 + 4 * c[1] * t**3 + 3 * c[2] * t**2 + 2 * c[3] * t + c[4] + def calculate_acceleration(c, t): """ @@ -146,7 +164,8 @@ def calculate_acceleration(c, t): Returns Acceleration """ - return 20*c[0]*t**3 + 12*c[1]*t**2 + 6*c[2]*t + 2*c[3] + return 20 * c[0] * t**3 + 12 * c[1] * t**2 + 6 * c[2] * t + 2 * c[3] + def rotation_matrix(roll, pitch, yaw): """ @@ -161,10 +180,12 @@ def rotation_matrix(roll, pitch, yaw): 3x3 rotation matrix as NumPy array """ return np.array( - [[cos(yaw)*cos(pitch), -sin(yaw)*cos(roll)+cos(yaw)*sin(pitch)*sin(roll), sin(yaw)*sin(roll)+cos(yaw)*sin(pitch)*cos(roll)], - [sin(yaw)*cos(pitch), cos(yaw)*cos(roll)+sin(yaw)*sin(pitch)*sin(roll), -cos(yaw)*sin(roll)+sin(yaw)*sin(pitch)*cos(roll)], - [-sin(pitch), cos(pitch)*sin(roll), cos(pitch)*cos(yaw)] - ]) + [[cos(yaw) * cos(pitch), -sin(yaw) * cos(roll) + cos(yaw) * sin(pitch) * sin(roll), sin(yaw) * sin(roll) + cos(yaw) * sin(pitch) * cos(roll)], + [sin(yaw) * cos(pitch), cos(yaw) * cos(roll) + sin(yaw) * sin(pitch) * + sin(roll), -cos(yaw) * sin(roll) + sin(yaw) * sin(pitch) * cos(roll)], + [-sin(pitch), cos(pitch) * sin(roll), cos(pitch) * cos(yaw)] + ]) + def main(): """ @@ -177,7 +198,7 @@ def main(): waypoints = [[-5, -5, 5], [5, -5, 5], [5, 5, 5], [-5, 5, 5]] for i in range(4): - traj = TrajectoryGenerator(waypoints[i], waypoints[(i+1)%4], T) + traj = TrajectoryGenerator(waypoints[i], waypoints[(i + 1) % 4], T) traj.solve() x_coeffs[i] = traj.x_c y_coeffs[i] = traj.y_c @@ -185,5 +206,6 @@ def main(): quad_sim(x_coeffs, y_coeffs, z_coeffs) + if __name__ == "__main__": main() \ No newline at end of file diff --git a/RoboticArm/NLinkArm.py b/ArmNavigation/n_joint_arm_to_point_control/NLinkArm.py similarity index 51% rename from RoboticArm/NLinkArm.py rename to ArmNavigation/n_joint_arm_to_point_control/NLinkArm.py index 4ab56054..71cab16c 100644 --- a/RoboticArm/NLinkArm.py +++ b/ArmNavigation/n_joint_arm_to_point_control/NLinkArm.py @@ -7,50 +7,61 @@ Author: Daniel Ingram import numpy as np import matplotlib.pyplot as plt + class NLinkArm(object): - def __init__(self, link_lengths, joint_angles, goal): + def __init__(self, link_lengths, joint_angles, goal, show_animation): + self.show_animation = show_animation self.n_links = len(link_lengths) if self.n_links is not len(joint_angles): raise ValueError() self.link_lengths = np.array(link_lengths) self.joint_angles = np.array(joint_angles) - self.points = [[0, 0] for _ in range(self.n_links+1)] + self.points = [[0, 0] for _ in range(self.n_links + 1)] self.lim = sum(link_lengths) self.goal = np.array(goal).T - self.fig = plt.figure() - self.fig.canvas.mpl_connect('button_press_event', self.click) + if show_animation: + self.fig = plt.figure() + self.fig.canvas.mpl_connect('button_press_event', self.click) - plt.ion() - plt.show() + plt.ion() + plt.show() self.update_points() def update_joints(self, joint_angles): self.joint_angles = joint_angles + self.update_points() def update_points(self): - for i in range(1, self.n_links+1): - self.points[i][0] = self.points[i-1][0] + self.link_lengths[i-1]*np.cos(np.sum(self.joint_angles[:i])) - self.points[i][1] = self.points[i-1][1] + self.link_lengths[i-1]*np.sin(np.sum(self.joint_angles[:i])) + for i in range(1, self.n_links + 1): + self.points[i][0] = self.points[i - 1][0] + \ + self.link_lengths[i - 1] * \ + np.cos(np.sum(self.joint_angles[:i])) + self.points[i][1] = self.points[i - 1][1] + \ + self.link_lengths[i - 1] * \ + np.sin(np.sum(self.joint_angles[:i])) self.end_effector = np.array(self.points[self.n_links]).T - self.plot() + if self.show_animation: + self.plot() def plot(self): plt.cla() - for i in range(self.n_links+1): + for i in range(self.n_links + 1): if i is not self.n_links: - plt.plot([self.points[i][0], self.points[i+1][0]], [self.points[i][1], self.points[i+1][1]], 'r-') + plt.plot([self.points[i][0], self.points[i + 1][0]], + [self.points[i][1], self.points[i + 1][1]], 'r-') plt.plot(self.points[i][0], self.points[i][1], 'ko') plt.plot(self.goal[0], self.goal[1], 'gx') - plt.plot([self.end_effector[0], self.goal[0]], [self.end_effector[1], self.goal[1]], 'g--') + plt.plot([self.end_effector[0], self.goal[0]], [ + self.end_effector[1], self.goal[1]], 'g--') plt.xlim([-self.lim, self.lim]) plt.ylim([-self.lim, self.lim]) diff --git a/ArmNavigation/n_joint_arm_to_point_control/animation.gif b/ArmNavigation/n_joint_arm_to_point_control/animation.gif new file mode 100644 index 00000000..a5de81fd Binary files /dev/null and b/ArmNavigation/n_joint_arm_to_point_control/animation.gif differ diff --git a/RoboticArm/n_link_arm_ik.py b/ArmNavigation/n_joint_arm_to_point_control/n_joint_arm_to_point_control.py similarity index 50% rename from RoboticArm/n_link_arm_ik.py rename to ArmNavigation/n_joint_arm_to_point_control/n_joint_arm_to_point_control.py index 5089d111..33fdf8c8 100644 --- a/RoboticArm/n_link_arm_ik.py +++ b/ArmNavigation/n_joint_arm_to_point_control/n_joint_arm_to_point_control.py @@ -2,31 +2,35 @@ Inverse kinematics for an n-link arm using the Jacobian inverse method Author: Daniel Ingram (daniel-s-ingram) + Atsushi Sakai (@Atsushi_twi) """ import matplotlib.pyplot as plt import numpy as np from NLinkArm import NLinkArm -#Simulation parameters +# Simulation parameters Kp = 2 dt = 0.1 N_LINKS = 10 N_ITERATIONS = 10000 -#States +# States WAIT_FOR_NEW_GOAL = 1 MOVING_TO_GOAL = 2 -def n_link_arm_ik(): +show_animation = True + + +def main(): """ Creates an arm using the NLinkArm class and uses its inverse kinematics to move it to the desired position. """ - link_lengths = [1]*N_LINKS - joint_angles = np.array([0]*N_LINKS) + link_lengths = [1] * N_LINKS + joint_angles = np.array([0] * N_LINKS) goal_pos = [N_LINKS, 0] - arm = NLinkArm(link_lengths, joint_angles, goal_pos) + arm = NLinkArm(link_lengths, joint_angles, goal_pos, show_animation) state = WAIT_FOR_NEW_GOAL solution_found = False while True: @@ -35,10 +39,11 @@ def n_link_arm_ik(): end_effector = arm.end_effector errors, distance = distance_to_goal(end_effector, goal_pos) - #State machine to allow changing of goal before current goal has been reached + # State machine to allow changing of goal before current goal has been reached if state is WAIT_FOR_NEW_GOAL: if distance > 0.1 and not solution_found: - joint_goal_angles, solution_found = inverse_kinematics(link_lengths, joint_angles, goal_pos) + joint_goal_angles, solution_found = inverse_kinematics( + link_lengths, joint_angles, goal_pos) if not solution_found: print("Solution could not be found.") state = WAIT_FOR_NEW_GOAL @@ -47,13 +52,14 @@ def n_link_arm_ik(): state = MOVING_TO_GOAL elif state is MOVING_TO_GOAL: if distance > 0.1 and (old_goal is goal_pos): - joint_angles = joint_angles + Kp*ang_diff(joint_goal_angles, joint_angles)*dt + joint_angles = joint_angles + Kp * \ + ang_diff(joint_goal_angles, joint_angles) * dt else: state = WAIT_FOR_NEW_GOAL solution_found = False arm.update_joints(joint_angles) - + def inverse_kinematics(link_lengths, joint_angles, goal_pos): """ @@ -69,34 +75,90 @@ def inverse_kinematics(link_lengths, joint_angles, goal_pos): joint_angles = joint_angles + np.matmul(J, errors) return joint_angles, False + +def get_random_goal(): + from random import random + SAREA = 15.0 + return [SAREA * random() - SAREA / 2.0, + SAREA * random() - SAREA / 2.0] + + +def animation(): + link_lengths = [1] * N_LINKS + joint_angles = np.array([0] * N_LINKS) + goal_pos = get_random_goal() + arm = NLinkArm(link_lengths, joint_angles, goal_pos, show_animation) + state = WAIT_FOR_NEW_GOAL + solution_found = False + + i_goal = 0 + while True: + old_goal = goal_pos + goal_pos = arm.goal + end_effector = arm.end_effector + errors, distance = distance_to_goal(end_effector, goal_pos) + + # State machine to allow changing of goal before current goal has been reached + if state is WAIT_FOR_NEW_GOAL: + + if distance > 0.1 and not solution_found: + joint_goal_angles, solution_found = inverse_kinematics( + link_lengths, joint_angles, goal_pos) + if not solution_found: + print("Solution could not be found.") + state = WAIT_FOR_NEW_GOAL + arm.goal = get_random_goal() + elif solution_found: + state = MOVING_TO_GOAL + elif state is MOVING_TO_GOAL: + if distance > 0.1 and (old_goal is goal_pos): + joint_angles = joint_angles + Kp * \ + ang_diff(joint_goal_angles, joint_angles) * dt + else: + state = WAIT_FOR_NEW_GOAL + solution_found = False + arm.goal = get_random_goal() + i_goal += 1 + + if i_goal >= 5: + break + + arm.update_joints(joint_angles) + + def forward_kinematics(link_lengths, joint_angles): x = y = 0 - for i in range(1, N_LINKS+1): - x += link_lengths[i-1]*np.cos(np.sum(joint_angles[:i])) - y += link_lengths[i-1]*np.sin(np.sum(joint_angles[:i])) + for i in range(1, N_LINKS + 1): + x += link_lengths[i - 1] * np.cos(np.sum(joint_angles[:i])) + y += link_lengths[i - 1] * np.sin(np.sum(joint_angles[:i])) return np.array([x, y]).T + def jacobian_inverse(link_lengths, joint_angles): J = np.zeros((2, N_LINKS)) for i in range(N_LINKS): J[0, i] = 0 J[1, i] = 0 for j in range(i, N_LINKS): - J[0, i] -= link_lengths[j]*np.sin(np.sum(joint_angles[:j])) - J[1, i] += link_lengths[j]*np.cos(np.sum(joint_angles[:j])) + J[0, i] -= link_lengths[j] * np.sin(np.sum(joint_angles[:j])) + J[1, i] += link_lengths[j] * np.cos(np.sum(joint_angles[:j])) return np.linalg.pinv(J) + def distance_to_goal(current_pos, goal_pos): x_diff = goal_pos[0] - current_pos[0] y_diff = goal_pos[1] - current_pos[1] return np.array([x_diff, y_diff]).T, np.math.sqrt(x_diff**2 + y_diff**2) + def ang_diff(theta1, theta2): """ Returns the difference between two angles in the range -pi to +pi """ - return (theta1 - theta2 + np.pi)%(2*np.pi) - np.pi + return (theta1 - theta2 + np.pi) % (2 * np.pi) - np.pi + if __name__ == '__main__': - n_link_arm_ik() \ No newline at end of file + main() + # animation() \ No newline at end of file diff --git a/ArmNavigation/two_joint_arm_to_point_control/animation.gif b/ArmNavigation/two_joint_arm_to_point_control/animation.gif new file mode 100644 index 00000000..e279420d Binary files /dev/null and b/ArmNavigation/two_joint_arm_to_point_control/animation.gif differ diff --git a/ArmNavigation/two_joint_arm_to_point_control/two_joint_arm_to_point_control.py b/ArmNavigation/two_joint_arm_to_point_control/two_joint_arm_to_point_control.py new file mode 100644 index 00000000..1468fc6d --- /dev/null +++ b/ArmNavigation/two_joint_arm_to_point_control/two_joint_arm_to_point_control.py @@ -0,0 +1,122 @@ +""" +Inverse kinematics of a two-joint arm +Left-click the plot to set the goal position of the end effector + +Author: Daniel Ingram (daniel-s-ingram) + Atsushi Sakai (@Atsushi_twi) + +Ref: P. I. Corke, "Robotics, Vision & Control", Springer 2017, ISBN 978-3-319-54413-7 p102 +- [Robotics, Vision and Control \| SpringerLink](https://link.springer.com/book/10.1007/978-3-642-20144-8) + +""" + +import matplotlib.pyplot as plt +import numpy as np + + +# Similation parameters +Kp = 15 +dt = 0.01 + +# Link lengths +l1 = l2 = 1 + +# Set initial goal position to the initial end-effector position +x = 2 +y = 0 + +show_animation = True + +if show_animation: + plt.ion() + + +def two_joint_arm(GOAL_TH=0.0, theta1=0.0, theta2=0.0): + """ + Computes the inverse kinematics for a planar 2DOF arm + """ + while True: + try: + theta2_goal = np.arccos( + (x**2 + y**2 - l1**2 - l2**2) / (2 * l1 * l2)) + theta1_goal = np.math.atan2(y, x) - np.math.atan2(l2 * + np.sin(theta2_goal), (l1 + l2 * np.cos(theta2_goal))) + + if theta1_goal < 0: + theta2_goal = -theta2_goal + theta1_goal = np.math.atan2( + y, x) - np.math.atan2(l2 * np.sin(theta2_goal), (l1 + l2 * np.cos(theta2_goal))) + + theta1 = theta1 + Kp * ang_diff(theta1_goal, theta1) * dt + theta2 = theta2 + Kp * ang_diff(theta2_goal, theta2) * dt + except ValueError as e: + print("Unreachable goal") + + wrist = plot_arm(theta1, theta2, x, y) + + # check goal + d2goal = np.math.sqrt((wrist[0] - x)**2 + (wrist[1] - y)**2) + + if abs(d2goal) < GOAL_TH and x is not None: + return theta1, theta2 + + +def plot_arm(theta1, theta2, x, y): + shoulder = np.array([0, 0]) + elbow = shoulder + np.array([l1 * np.cos(theta1), l1 * np.sin(theta1)]) + wrist = elbow + \ + np.array([l2 * np.cos(theta1 + theta2), l2 * np.sin(theta1 + theta2)]) + + if show_animation: + plt.cla() + + plt.plot([shoulder[0], elbow[0]], [shoulder[1], elbow[1]], 'k-') + plt.plot([elbow[0], wrist[0]], [elbow[1], wrist[1]], 'k-') + + plt.plot(shoulder[0], shoulder[1], 'ro') + plt.plot(elbow[0], elbow[1], 'ro') + plt.plot(wrist[0], wrist[1], 'ro') + + plt.plot([wrist[0], x], [wrist[1], y], 'g--') + plt.plot(x, y, 'g*') + + plt.xlim(-2, 2) + plt.ylim(-2, 2) + + plt.show() + plt.pause(dt) + + return wrist + + +def ang_diff(theta1, theta2): + # Returns the difference between two angles in the range -pi to +pi + return (theta1 - theta2 + np.pi) % (2 * np.pi) - np.pi + + +def click(event): + global x, y + x = event.xdata + y = event.ydata + + +def animation(): + from random import random + global x, y + theta1 = theta2 = 0.0 + for i in range(5): + x = 2.0 * random() - 1.0 + y = 2.0 * random() - 1.0 + theta1, theta2 = two_joint_arm( + GOAL_TH=0.01, theta1=theta1, theta2=theta2) + + +def main(): + fig = plt.figure() + fig.canvas.mpl_connect("button_press_event", click) + two_joint_arm() + + +if __name__ == "__main__": + # animation() + main() diff --git a/Localization/Kalmanfilter_basics.ipynb b/Localization/Kalmanfilter_basics.ipynb new file mode 100644 index 00000000..b37dd130 --- /dev/null +++ b/Localization/Kalmanfilter_basics.ipynb @@ -0,0 +1,765 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# _KF Basics - Part I_\n", + "----\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### What is the need to describe belief in terms of PDF's?\n", + "This is because robot environments are stochastic. A robot environment may have cows with Tesla by side. That is a robot and it's environment cannot be deterministically modelled(e.g as a function of something like time t). In the real world sensors are also error prone, and hence there'll be a set of values with a mean and variance that it can take. Hence, we always have to model around some mean and variances associated." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### What is Expectation of a Random Variables?\n", + " Expectation is nothing but an average of the probabilites\n", + " \n", + "$$\\mathbb E[X] = \\sum_{i=1}^n p_ix_i$$\n", + "\n", + "In the continous form,\n", + "\n", + "$$\\mathbb E[X] = \\int_{-\\infty}^\\infty x\\, f(x) \\,dx$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.4000000000000001\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "import random\n", + "x=[3,1,2]\n", + "p=[0.1,0.3,0.4]\n", + "E_x=np.sum(np.multiply(x,p))\n", + "print(E_x)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### What is the advantage of representing the belief as a unimodal as opposed to multimodal?\n", + "Obviously, it makes sense because we can't multiple probabilities to a car moving for two locations. This would be too confusing and the information will not be useful." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Variance, Covariance and Correlation\n", + "----\n", + "### Variance\n", + "Variance is the spread of the data. The mean does'nt tell much **about** the data. Therefore the variance tells us about the **story** about the data meaning the spread of the data.\n", + "\n", + "$$\\mathit{VAR}(X) = \\frac{1}{n}\\sum_{i=1}^n (x_i - \\mu)^2$$" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1.0224618077401504" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x=np.random.randn(10)\n", + "np.var(x)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Covariance\n", + "\n", + "This is for a multivariate distribution. For example, a robot in 2-D space can take values in both x and y. To describe them, a normal distribution with mean in both x and y is needed.\n", + "\n", + "For a multivariate distribution, mean $\\mu can be represented as a matrix, \n", + "\n", + "$$\n", + "\\mu = \\begin{bmatrix}\\mu_1\\\\\\mu_2\\\\ \\vdots \\\\\\mu_n\\end{bmatrix}\n", + "$$\n", + "\n", + "\n", + "Similarly, variance can also be represented.\n", + "\n", + "But an important concept is that in the same way as every variable or dimension has a variation in its values, it is also possible that there will be values on how they **together vary**. This is also a measure of how two datasets are related to each other or **correlation**.\n", + "\n", + "For example, as height increases weight also generally increases. These variables are correlated. They are positively correlated because as one variable gets larger so does the other.\n", + "\n", + "We use a **covariance matrix** to denote covariances of a multivariate normal distribution:\n", + "$$\n", + "\\Sigma = \\begin{bmatrix}\n", + " \\sigma_1^2 & \\sigma_{12} & \\cdots & \\sigma_{1n} \\\\\n", + " \\sigma_{21} &\\sigma_2^2 & \\cdots & \\sigma_{2n} \\\\\n", + " \\vdots & \\vdots & \\ddots & \\vdots \\\\\n", + " \\sigma_{n1} & \\sigma_{n2} & \\cdots & \\sigma_n^2\n", + " \\end{bmatrix}\n", + "$$\n", + "\n", + "**Diagonal** - Variance of each variable associated. \n", + "\n", + "**Off-Diagonal** - covariance between ith and jth variables.\n", + "\n", + "$$\\begin{aligned}VAR(X) = \\sigma_x^2 &= \\frac{1}{n}\\sum_{i=1}^n(X - \\mu)^2\\\\\n", + "COV(X, Y) = \\sigma_{xy} &= \\frac{1}{n}\\sum_{i=1}^n[(X-\\mu_x)(Y-\\mu_y)\\big]\\end{aligned}$$" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0.08868895, 0.05064471, 0.08855629],\n", + " [0.05064471, 0.06219243, 0.11555291],\n", + " [0.08855629, 0.11555291, 0.21534324]])" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x=np.random.random((3,3))\n", + "np.cov(x)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Covariance taking the data as **sample** with $\\frac{1}{N-1}$" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[ 0.1571437 , -0.00766623],\n", + " [-0.00766623, 0.13957621]])" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x_cor=np.random.rand(1,10)\n", + "y_cor=np.random.rand(1,10)\n", + "np.cov(x_cor,y_cor)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Covariance taking the data as **population** with $\\frac{1}{N}$" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[ 0.14142933, -0.0068996 ],\n", + " [-0.0068996 , 0.12561859]])" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np.cov(x_cor,y_cor,bias=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Gaussians \n", + "----\n", + "\n", + "\n", + "\n", + "#### Central Limit Theorem\n", + "\n", + "According to this theorem, the average of n samples of random and independant variables tends to follow a normal distribution as we increase the sample size.(Generally, for n>=30)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(array([ 1., 4., 9., 12., 12., 20., 16., 16., 4., 6.]),\n", + " array([5.30943011, 5.34638597, 5.38334183, 5.42029769, 5.45725355,\n", + " 5.49420941, 5.53116527, 5.56812114, 5.605077 , 5.64203286,\n", + " 5.67898872]),\n", + " )" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import random\n", + "a=np.zeros((100,))\n", + "for i in range(100):\n", + " x=[random.uniform(1,10) for _ in range(1000)]\n", + " a[i]=np.sum(x,axis=0)/1000\n", + "plt.hist(a)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Gaussian Distribution\n", + "A Gaussian is a *continuous probability distribution* that is completely described with two parameters, the mean ($\\mu$) and the variance ($\\sigma^2$). It is defined as:\n", + "\n", + "$$ \n", + "f(x, \\mu, \\sigma) = \\frac{1}{\\sigma\\sqrt{2\\pi}} \\exp\\big [{-\\frac{(x-\\mu)^2}{2\\sigma^2} }\\big ]\n", + "$$\n", + "Range is [$-\\inf,\\inf $]\n", + "\n", + "\n", + "This is just a function of mean($\\mu$) and standard deviation ($\\sigma$) and what gives the normal distribution the charecteristic **bell curve**. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.mlab as mlab\n", + "import math\n", + "import scipy.stats\n", + "\n", + "mu = 0\n", + "variance = 5\n", + "sigma = math.sqrt(variance)\n", + "x = np.linspace(mu - 5*sigma, mu + 5*sigma, 100)\n", + "plt.plot(x,scipy.stats.norm.pdf(x, mu, sigma))\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Why do we need Gaussian distributions?\n", + "Since it becomes really difficult in the real world to deal with multimodal distribution as we cannot put the belief in two seperate location of the robots. This becomes really confusing and in practice impossible to comprehend. \n", + "Gaussian probability distribution allows us to drive the robots using only one mode with peak at the mean with some variance." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Gaussian Properties\n", + "----\n", + "**Multiplication**\n", + "\n", + "\n", + "For the measurement update in a Bayes Filter, the algorithm tells us to multiply the Prior P(X_t) and measurement P(Z_t|X_t) to calculate the posterior:\n", + "\n", + "$$P(X \\mid Z) = \\frac{P(Z \\mid X)P(X)}{P(Z)}$$\n", + "\n", + "Here for the numerator, $P(Z \\mid X),P(X)$ both are gaussian.\n", + "\n", + "$N(\\bar\\mu, \\bar\\sigma^1)$ and $N(\\bar\\mu, \\bar\\sigma^2)$ are their mean and variances.\n", + "\n", + "New mean is \n", + "\n", + "$$\\mu_\\mathtt{new} = \\frac{\\sigma_z^2\\bar\\mu + \\bar\\sigma^2z}{\\bar\\sigma^2+\\sigma_z^2}$$\n", + "New variance is\n", + "$$\n", + "\\sigma_\\mathtt{new} = \\frac{\\sigma_z^2\\bar\\sigma^2}{\\bar\\sigma^2+\\sigma_z^2}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New mean is at: 5.0\n", + "New variance is: 1.0\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.mlab as mlab\n", + "import math\n", + "mu1 = 0\n", + "variance1 = 2\n", + "sigma = math.sqrt(variance1)\n", + "x1 = np.linspace(mu1 - 3*sigma, mu1 + 3*sigma, 100)\n", + "plt.plot(x1,scipy.stats.norm.pdf(x1, mu1, sigma),label='prior')\n", + "\n", + "mu2 = 10\n", + "variance2 = 2\n", + "sigma = math.sqrt(variance2)\n", + "x2 = np.linspace(mu2 - 3*sigma, mu2 + 3*sigma, 100)\n", + "plt.plot(x2,scipy.stats.norm.pdf(x2, mu2, sigma),\"g-\",label='measurement')\n", + "\n", + "\n", + "mu_new=(mu1*variance2+mu2*variance1)/(variance1+variance2)\n", + "print(\"New mean is at: \",mu_new)\n", + "var_new=(variance1*variance2)/(variance1+variance2)\n", + "print(\"New variance is: \",var_new)\n", + "sigma = math.sqrt(var_new)\n", + "x3 = np.linspace(mu_new - 3*sigma, mu_new + 3*sigma, 100)\n", + "plt.plot(x3,scipy.stats.norm.pdf(x3, mu_new, var_new),label=\"posterior\")\n", + "plt.legend(loc='upper left')\n", + "plt.xlim(-10,20)\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Addition**\n", + "\n", + "The motion step involves a case of adding up probability (Since it has to abide the Law of Total Probability). This means their beliefs are to be added and hence two gaussians. They are simply arithmetic additions of the two.\n", + "\n", + "$$\\begin{gathered}\\mu_x = \\mu_p + \\mu_z \\\\\n", + "\\sigma_x^2 = \\sigma_z^2+\\sigma_p^2\\, \\square\\end{gathered}$$" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New mean is at: 15\n", + "New variance is: 2\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.mlab as mlab\n", + "import math\n", + "mu1 = 5\n", + "variance1 = 1\n", + "sigma = math.sqrt(variance1)\n", + "x1 = np.linspace(mu1 - 3*sigma, mu1 + 3*sigma, 100)\n", + "plt.plot(x1,scipy.stats.norm.pdf(x1, mu1, sigma),label='prior')\n", + "\n", + "mu2 = 10\n", + "variance2 = 1\n", + "sigma = math.sqrt(variance2)\n", + "x2 = np.linspace(mu2 - 3*sigma, mu2 + 3*sigma, 100)\n", + "plt.plot(x2,scipy.stats.norm.pdf(x2, mu2, sigma),\"g-\",label='measurement')\n", + "\n", + "\n", + "mu_new=mu1+mu2\n", + "print(\"New mean is at: \",mu_new)\n", + "var_new=(variance1+variance2)\n", + "print(\"New variance is: \",var_new)\n", + "sigma = math.sqrt(var_new)\n", + "x3 = np.linspace(mu_new - 3*sigma, mu_new + 3*sigma, 100)\n", + "plt.plot(x3,scipy.stats.norm.pdf(x3, mu_new, var_new),label=\"posterior\")\n", + "plt.legend(loc='upper left')\n", + "plt.xlim(-10,20)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 188, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#Example from:\n", + "#https://scipython.com/blog/visualizing-the-bivariate-gaussian-distribution/\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import cm\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "\n", + "# Our 2-dimensional distribution will be over variables X and Y\n", + "N = 60\n", + "X = np.linspace(-3, 3, N)\n", + "Y = np.linspace(-3, 4, N)\n", + "X, Y = np.meshgrid(X, Y)\n", + "\n", + "# Mean vector and covariance matrix\n", + "mu = np.array([0., 1.])\n", + "Sigma = np.array([[ 1. , -0.5], [-0.5, 1.5]])\n", + "\n", + "# Pack X and Y into a single 3-dimensional array\n", + "pos = np.empty(X.shape + (2,))\n", + "pos[:, :, 0] = X\n", + "pos[:, :, 1] = Y\n", + "\n", + "def multivariate_gaussian(pos, mu, Sigma):\n", + " \"\"\"Return the multivariate Gaussian distribution on array pos.\n", + "\n", + " pos is an array constructed by packing the meshed arrays of variables\n", + " x_1, x_2, x_3, ..., x_k into its _last_ dimension.\n", + "\n", + " \"\"\"\n", + "\n", + " n = mu.shape[0]\n", + " Sigma_det = np.linalg.det(Sigma)\n", + " Sigma_inv = np.linalg.inv(Sigma)\n", + " N = np.sqrt((2*np.pi)**n * Sigma_det)\n", + " # This einsum call calculates (x-mu)T.Sigma-1.(x-mu) in a vectorized\n", + " # way across all the input variables.\n", + " fac = np.einsum('...k,kl,...l->...', pos-mu, Sigma_inv, pos-mu)\n", + "\n", + " return np.exp(-fac / 2) / N\n", + "\n", + "# The distribution on the variables X, Y packed into pos.\n", + "Z = multivariate_gaussian(pos, mu, Sigma)\n", + "\n", + "# Create a surface plot and projected filled contour plot under it.\n", + "fig = plt.figure()\n", + "ax = fig.gca(projection='3d')\n", + "ax.plot_surface(X, Y, Z, rstride=3, cstride=3, linewidth=1, antialiased=True,\n", + " cmap=cm.viridis)\n", + "\n", + "cset = ax.contourf(X, Y, Z, zdir='z', offset=-0.15, cmap=cm.viridis)\n", + "\n", + "# Adjust the limits, ticks and view angle\n", + "ax.set_zlim(-0.15,0.2)\n", + "ax.set_zticks(np.linspace(0,0.2,5))\n", + "ax.view_init(27, -21)\n", + "\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a 3D projection of the gaussians involved with the lower surface showing the 2D projection of the 3D projection above. The innermost ellipse represents the highest peak, that is the maximum probability for a given (X,Y) value.\n", + "\n", + "\n", + "\n", + "\n", + "##### numpy einsum examples" + ] + }, + { + "cell_type": "code", + "execution_count": 175, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[ 0 1 2 3 4]\n", + " [ 5 6 7 8 9]\n", + " [10 11 12 13 14]\n", + " [15 16 17 18 19]\n", + " [20 21 22 23 24]]\n", + "[0 1 2 3 4]\n", + "[[0 1 2]\n", + " [3 4 5]]\n" + ] + } + ], + "source": [ + "a = np.arange(25).reshape(5,5)\n", + "b = np.arange(5)\n", + "c = np.arange(6).reshape(2,3)\n", + "print(a)\n", + "print(b)\n", + "print(c)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 204, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 30, 80, 130, 180, 230])" + ] + }, + "execution_count": 204, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#this is the diagonal sum, i repeated means the diagonal\n", + "np.einsum('ij', a)\n", + "#this takes the output ii which is the diagonal and outputs to a\n", + "np.einsum('ii->i',a)\n", + "#this takes in the array A represented by their axes 'ij' and B by its only axes'j' \n", + "#and multiples them element wise\n", + "np.einsum('ij,j',a, b)" + ] + }, + { + "cell_type": "code", + "execution_count": 199, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 0, 22, 76])" + ] + }, + "execution_count": 199, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A = np.arange(3).reshape(3,1)\n", + "B = np.array([[ 0, 1, 2, 3],\n", + " [ 4, 5, 6, 7],\n", + " [ 8, 9, 10, 11]])\n", + "C=np.multiply(A,B)\n", + "np.sum(C,axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 197, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 0, 22, 76])" + ] + }, + "execution_count": 197, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "D = np.array([0,1,2])\n", + "E = np.array([[ 0, 1, 2, 3],\n", + " [ 4, 5, 6, 7],\n", + " [ 8, 9, 10, 11]])\n", + "\n", + "np.einsum('i,ij->i',D,E)" + ] + }, + { + "cell_type": "code", + "execution_count": 238, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 238, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from scipy.stats import multivariate_normal\n", + "x, y = np.mgrid[-5:5:.1, -5:5:.1]\n", + "pos = np.empty(x.shape + (2,))\n", + "pos[:, :, 0] = x; pos[:, :, 1] = y\n", + "rv = multivariate_normal([0.5, -0.2], [[2.0, 0.9], [0.9, 0.5]])\n", + "plt.contourf(x, y, rv.pdf(pos))\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### References:\n", + "\n", + "1. Roger Labbe's [repo](https://github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python) on Kalman Filters. (Majority of the examples in the notes are from this)\n", + "\n", + "\n", + "\n", + "2. Probabilistic Robotics by Sebastian Thrun, Wolfram Burgard and Dieter Fox, MIT Press.\n", + "\n", + "\n", + "\n", + "3. Scipy [Documentation](https://scipython.com/blog/visualizing-the-bivariate-gaussian-distribution/)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Localization/Kalmanfilter_basics_2.ipynb b/Localization/Kalmanfilter_basics_2.ipynb new file mode 100644 index 00000000..099b71d9 --- /dev/null +++ b/Localization/Kalmanfilter_basics_2.ipynb @@ -0,0 +1,347 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# _KF Basics - Part 2_\n", + "-------" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ### Probabilistic Generative Laws\n", + " ------------\n", + "**1st Law**:\n", + "The belief representing the state $x_{t}$, is conditioned on all past states, measurements and controls. This can be shown mathematically by the conditional probability shown below:\n", + "\n", + "$$p(x_{t} | x_{0:t-1},z_{1:t-1},u_{1:t})$$\n", + "\n", + "1) $z_{t}$ represents the **measurement**\n", + "\n", + "2) $u_{t}$ the **motion command**\n", + "\n", + "3) $x_{t}$ the **state** (can be the position, velocity, etc) of the robot or its environment at time t.\n", + "\n", + "\n", + "'If we know the state $x_{t-1}$ and $u_{t}$, then knowing the states $x_{0:t-2}$, $z_{1:t-1}$ becomes immaterial through the property of **conditional independence**'. The state $x_{t-1}$ introduces a conditional independence between $x_{t}$ and $z_{1:t-1}$, $u_{1:t-1}$\n", + "\n", + "Therefore the law now holds as:\n", + "\n", + "$$p(x_{t} | x_{0:t-1},z_{1:t-1},u_{1:t})=p(x_{t} | x_{t-1},u_{t})$$\n", + "\n", + "**2nd Law**:\n", + "\n", + "If $x_{t}$ is complete, then:\n", + "\n", + "$$p(z_{t} | x-_{0:t},z_{1:t-1},u_{1:t})=p(z_{t} | x_{t})$$\n", + "\n", + "$x_{t}$ is **complete** means that the past states, controls or measurements carry no additional information to predict future.\n", + "\n", + "$x_{0:t-1}$, $z_{1:t-1}$ and $u_{1:t}$ are **conditionally independent** of $z_{t}$ given $x_{t}$ of complete.\n", + "\n", + "The filter works in two parts:\n", + "\n", + "$\\bullet$ $p(x_{t} | x_{t-1},u_{t})\\rightarrow $**State Transition Probability**\n", + "\n", + "$\\bullet$ $p(z_{t} | x_{t}) \\rightarrow $**Measurement Probability**\n", + "\n", + "\n", + "### Conditional dependence and independence example:\n", + "-------\n", + "\n", + "$\\bullet$**Independent but conditionally dependent**\n", + "\n", + "Let's say you flip two fair coins\n", + "\n", + "A - Your first coin flip is heads\n", + "\n", + "B - Your second coin flip is heads\n", + "\n", + "C - Your first two flips were the same\n", + "\n", + "\n", + "A and B here are independent. However, A and B are conditionally dependent given C, since if you know C then your first coin flip will inform the other one.\n", + "\n", + "$\\bullet$**Dependent but conditionally independent**\n", + "\n", + "A box contains two coins: a regular coin and one fake two-headed coin ((P(H)=1). I choose a coin at random and toss it twice. Define the following events.\n", + "\n", + "A= First coin toss results in an H.\n", + "\n", + "B= Second coin toss results in an H.\n", + "\n", + "C= Coin 1 (regular) has been selected. \n", + "\n", + "If we know A has occurred (i.e., the first coin toss has resulted in heads), we would guess that it is more likely that we have chosen Coin 2 than Coin 1. This in turn increases the conditional probability that B occurs. This suggests that A and B are not independent. On the other hand, given C (Coin 1 is selected), A and B are independent.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bayes Rule:\n", + "------------\n", + "\n", + "\n", + "Posterior = $\\frac{Likelihood*Prior}{Marginal} $\n", + "\n", + "Here,\n", + "\n", + "**Posterior** = Probability of an event occurring based on certain evidence.\n", + "\n", + "**Likelihood** = How probable is the evidence given the event.\n", + "\n", + "**Prior** = Probability of the just the event occurring without having any evidence.\n", + "\n", + "**Marginal** = Probability of the evidence given all the instances of events possible.\n", + "\n", + "\n", + "\n", + "Example:\n", + "\n", + "1% of women have breast cancer (and therefore 99% do not).\n", + "80% of mammograms detect breast cancer when it is there (and therefore 20% miss it).\n", + "9.6% of mammograms detect breast cancer when its not there (and therefore 90.4% correctly return a negative result).\n", + "\n", + "We can turn the process above into an equation, which is Bayes Theorem. Here is the equation:\n", + "\n", + "$\\displaystyle{\\Pr(\\mathrm{A}|\\mathrm{X}) = \\frac{\\Pr(\\mathrm{X}|\\mathrm{A})\\Pr(\\mathrm{A})}{\\Pr(\\mathrm{X|A})\\Pr(\\mathrm{A})+ \\Pr(\\mathrm{X | not \\ A})\\Pr(\\mathrm{not \\ A})}}$\n", + "\n", + "\n", + "$\\bullet$Pr(A|X) = Chance of having cancer (A) given a positive test (X). This is what we want to know: How likely is it to have cancer with a positive result? In our case it was 7.8%.\n", + "\n", + "$\\bullet$Pr(X|A) = Chance of a positive test (X) given that you had cancer (A). This is the chance of a true positive, 80% in our case.\n", + "\n", + "$\\bullet$Pr(A) = Chance of having cancer (1%).\n", + "\n", + "$\\bullet$Pr(not A) = Chance of not having cancer (99%).\n", + "\n", + "$\\bullet$Pr(X|not A) = Chance of a positive test (X) given that you didn't have cancer (~A). This is a false positive, 9.6% in our case.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bayes Filter Algorithm\n", + "-------\n", + "The basic filter algorithm is:\n", + "\n", + "for all $x_{t}$:\n", + "\n", + "1. $\\overline{bel}(x_t) = \\int p(x_t | u_t, x_{t-1}) bel(x_{t-1})dx$\n", + "\n", + "2. $bel(x_t) = \\eta p(z_t | x_t) \\overline{bel}(x_t)$\n", + "\n", + "end.\n", + "\n", + "\n", + "$\\rightarrow$The first step in filter is to calculate the prior for the next step that uses the bayes theorem. This is the **Prediction** step. The belief, $\\overline{bel}(x_t)$, is **before** incorporating measurement($z_{t}$) at time t=t. This is the step where the motion occurs and information is lost because the means and covariances of the gaussians are added. The RHS of the equation incorporates the law of total probability for prior calculation.\n", + "\n", + "$\\rightarrow$ This is the **Correction** or update step that calculates the belief of the robot **after** taking into account the measurement($z_{t}$) at time t=t. This is where we incorporate the sensor information for the whereabouts of the robot. We gain information here as the gaussians get multiplied here. (Multiplication of gaussian values allows the resultant to lie in between these numbers and the resultant covariance is smaller.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bayes filter localization example:\n", + "----\n", + "\n", + "\n", + "\n", + "\n", + "Given - A robot with a sensor to detect doorways along a hallway. Also, the robot knows how the hallway looks like but doesn't know where it is in the map. \n", + "\n", + "\n", + "1. Initially(first scenario), it doesn't know where it is with respect to the map and hence the belief assigns equal probability to each location in the map.\n", + "\n", + "\n", + "2. The first sensor reading is incorporated and it shows the presence of a door. Now the robot knows how the map looks like but cannot localize yet as map has 3 doors present. Therefore it assigns equal probability to each door present. \n", + "\n", + "\n", + "3. The robot now moves forward. This is the prediction step and the motion causes the robot to lose some of the information and hence the variance of the gaussians increase (diagram 4.). The final belief is **convolution** of posterior from previous step and the current state after motion. Also, the means shift on the right due to the motion.\n", + "\n", + "\n", + "4. Again, incorporating the measurement, the sensor senses a door and this time too the possibility of door is equal for the three door. This is where the filter's magic kicks in. For the final belief (diagram 5.), the posterior calculated after sensing is mixed or **convolution** of previous posterior and measurement. It improves the robot's belief at location near to the second door. The variance **decreases** and **peaks**.\n", + "\n", + "\n", + "5. Finally after series of iterations of motion and correction, the robot is able to localize itself with respect to the environment.(diagram 6.)\n", + "\n", + "Do note that the robot knows the map but doesn't know where exactly it is on the map." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bayes and Kalman filter structure\n", + "-----\n", + "The basic structure and the concept remains the same as bayes filter for Kalman. The only key difference is the mathematical representation of Kalman filter. The Kalman filter is nothing but a bayesian filter that uses Gaussians.\n", + "\n", + "For a bayes filter to be a Kalman filter, **each term of belief is now a gaussian**, unlike histograms. The basic formulation for the **bayes filter** algorithm is:\n", + "\n", + "$$\\begin{aligned} \n", + "\\bar {\\mathbf x} &= \\mathbf x \\ast f_{\\mathbf x}(\\bullet)\\, \\, &\\text{Prediction} \\\\\n", + "\\mathbf x &= \\mathcal L \\cdot \\bar{\\mathbf x}\\, \\, &\\text{Correction}\n", + "\\end{aligned}$$\n", + "\n", + "\n", + "$\\bar{\\mathbf x}$ is the *prior* \n", + "\n", + "$\\mathcal L$ is the *likelihood* of a measurement given the prior $\\bar{\\mathbf x}$\n", + "\n", + "$f_{\\mathbf x}(\\bullet)$ is the *process model* or the gaussian term that helps predict the next state like velocity\n", + "to track position or acceleration.\n", + "\n", + "$\\ast$ denotes *convolution*.\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Kalman Gain\n", + "-------\n", + "\n", + "$$ x = (\\mathcal L \\bar x)$$\n", + "\n", + "Where x is posterior and $\\mathcal L$ and $\\bar x$ are gaussians.\n", + "\n", + "Therefore the mean of the posterior is given by:\n", + "\n", + "$$\n", + "\\mu=\\frac{\\bar\\sigma^2\\, \\mu_z + \\sigma_z^2 \\, \\bar\\mu} {\\bar\\sigma^2 + \\sigma_z^2}\n", + "$$\n", + "\n", + "\n", + "$$\\mu = \\left( \\frac{\\bar\\sigma^2}{\\bar\\sigma^2 + \\sigma_z^2}\\right) \\mu_z + \\left(\\frac{\\sigma_z^2}{\\bar\\sigma^2 + \\sigma_z^2}\\right)\\bar\\mu$$\n", + "\n", + "In this form it is easy to see that we are scaling the measurement and the prior by weights: \n", + "\n", + "$$\\mu = W_1 \\mu_z + W_2 \\bar\\mu$$\n", + "\n", + "\n", + "The weights sum to one because the denominator is a normalization term. We introduce a new term, $K=W_1$, giving us:\n", + "\n", + "$$\\begin{aligned}\n", + "\\mu &= K \\mu_z + (1-K) \\bar\\mu\\\\\n", + "&= \\bar\\mu + K(\\mu_z - \\bar\\mu)\n", + "\\end{aligned}$$\n", + "\n", + "where\n", + "\n", + "$$K = \\frac {\\bar\\sigma^2}{\\bar\\sigma^2 + \\sigma_z^2}$$\n", + "\n", + "The variance in terms of the Kalman gain:\n", + "\n", + "$$\\begin{aligned}\n", + "\\sigma^2 &= \\frac{\\bar\\sigma^2 \\sigma_z^2 } {\\bar\\sigma^2 + \\sigma_z^2} \\\\\n", + "&= K\\sigma_z^2 \\\\\n", + "&= (1-K)\\bar\\sigma^2 \n", + "\\end{aligned}$$\n", + "\n", + "\n", + "**$K$ is the *Kalman gain*. It's the crux of the Kalman filter. It is a scaling term that chooses a value partway between $\\mu_z$ and $\\bar\\mu$.**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Kalman Filter - Univariate and Multivariate\n", + "-----\n", + "\n", + "**Prediction**\n", + "\n", + "$\\begin{array}{|l|l|l|}\n", + "\\hline\n", + "\\text{Univariate} & \\text{Univariate} & \\text{Multivariate}\\\\\n", + "& \\text{(Kalman form)} & \\\\\n", + "\\hline\n", + "\\bar \\mu = \\mu + \\mu_{f_x} & \\bar x = x + dx & \\bar{\\mathbf x} = \\mathbf{Fx} + \\mathbf{Bu}\\\\\n", + "\\bar\\sigma^2 = \\sigma_x^2 + \\sigma_{f_x}^2 & \\bar P = P + Q & \\bar{\\mathbf P} = \\mathbf{FPF}^\\mathsf T + \\mathbf Q \\\\\n", + "\\hline\n", + "\\end{array}$\n", + "\n", + "$\\mathbf x,\\, \\mathbf P$ are the state mean and covariance. They correspond to $x$ and $\\sigma^2$.\n", + "\n", + "$\\mathbf F$ is the *state transition function*. When multiplied by $\\bf x$ it computes the prior. \n", + "\n", + "$\\mathbf Q$ is the process covariance. It corresponds to $\\sigma^2_{f_x}$.\n", + "\n", + "$\\mathbf B$ and $\\mathbf u$ are model control inputs to the system.\n", + "\n", + "**Correction**\n", + "\n", + "$\\begin{array}{|l|l|l|}\n", + "\\hline\n", + "\\text{Univariate} & \\text{Univariate} & \\text{Multivariate}\\\\\n", + "& \\text{(Kalman form)} & \\\\\n", + "\\hline\n", + "& y = z - \\bar x & \\mathbf y = \\mathbf z - \\mathbf{H\\bar x} \\\\\n", + "& K = \\frac{\\bar P}{\\bar P+R}&\n", + "\\mathbf K = \\mathbf{\\bar{P}H}^\\mathsf T (\\mathbf{H\\bar{P}H}^\\mathsf T + \\mathbf R)^{-1} \\\\\n", + "\\mu=\\frac{\\bar\\sigma^2\\, \\mu_z + \\sigma_z^2 \\, \\bar\\mu} {\\bar\\sigma^2 + \\sigma_z^2} & x = \\bar x + Ky & \\mathbf x = \\bar{\\mathbf x} + \\mathbf{Ky} \\\\\n", + "\\sigma^2 = \\frac{\\sigma_1^2\\sigma_2^2}{\\sigma_1^2+\\sigma_2^2} & P = (1-K)\\bar P &\n", + "\\mathbf P = (\\mathbf I - \\mathbf{KH})\\mathbf{\\bar{P}} \\\\\n", + "\\hline\n", + "\\end{array}$\n", + "\n", + "$\\mathbf H$ is the measurement function.\n", + "\n", + "$\\mathbf z,\\, \\mathbf R$ are the measurement mean and noise covariance. They correspond to $z$ and $\\sigma_z^2$ in the univariate filter. \n", + "$\\mathbf y$ and $\\mathbf K$ are the residual and Kalman gain. \n", + "\n", + "The details will be different than the univariate filter because these are vectors and matrices, but the concepts are exactly the same: \n", + "\n", + "- Use a Gaussian to represent our estimate of the state and error\n", + "- Use a Gaussian to represent the measurement and its error\n", + "- Use a Gaussian to represent the process model\n", + "- Use the process model to predict the next state (the prior)\n", + "- Form an estimate part way between the measurement and the prior" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### References:\n", + "-------\n", + "\n", + "1. Roger Labbe's [repo](https://github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python) on Kalman Filters. (Majority of text in the notes are from this)\n", + "\n", + "\n", + "\n", + "2. Probabilistic Robotics by Sebastian Thrun, Wolfram Burgard and Dieter Fox, MIT Press.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Localization/bayes_filter.png b/Localization/bayes_filter.png new file mode 100644 index 00000000..50e509bd Binary files /dev/null and b/Localization/bayes_filter.png differ diff --git a/Localization/extended_kalman_filter/extended_kalman_filter.py b/Localization/extended_kalman_filter/extended_kalman_filter.py index 3dd61e34..8c011862 100644 --- a/Localization/extended_kalman_filter/extended_kalman_filter.py +++ b/Localization/extended_kalman_filter/extended_kalman_filter.py @@ -80,12 +80,25 @@ def observation_model(x): def jacobF(x, u): - # Jacobian of Motion Model + """ + Jacobian of Motion Model + + motion model + x_{t+1} = x_t+v*dt*cos(yaw) + y_{t+1} = y_t+v*dt*sin(yaw) + yaw_{t+1} = yaw_t+omega*dt + v_{t+1} = v{t} + so + dx/dyaw = -v*dt*sin(yaw) + dx/dv = dt*cos(yaw) + dy/dyaw = v*dt*cos(yaw) + dy/dv = dt*sin(yaw) + """ yaw = x[2, 0] - u1 = u[0, 0] + v = u[0, 0] jF = np.matrix([ - [1.0, 0.0, -DT * u1 * math.sin(yaw), DT * u1 * math.cos(yaw)], - [0.0, 1.0, DT * math.cos(yaw), DT * math.sin(yaw)], + [1.0, 0.0, -DT * v * math.sin(yaw), DT * math.cos(yaw)], + [0.0, 1.0, DT * v * math.cos(yaw), DT * math.sin(yaw)], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]) diff --git a/Localization/extended_kalman_filter/notebook.ipynb b/Localization/extended_kalman_filter/notebook.ipynb new file mode 100644 index 00000000..303633e1 --- /dev/null +++ b/Localization/extended_kalman_filter/notebook.ipynb @@ -0,0 +1,48 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Extended Kalman Filter Localization\n", + "\n", + "Code; [PythonRobotics/extended\\_kalman\\_filter\\.py at master · AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/blob/master/Localization/extended_kalman_filter/extended_kalman_filter.py)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Kalman Filter" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Ref" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Localization/histogram_filter/animation.gif b/Localization/histogram_filter/animation.gif new file mode 100644 index 00000000..d868c411 Binary files /dev/null and b/Localization/histogram_filter/animation.gif differ diff --git a/Localization/histogram_filter/histogram_filter.py b/Localization/histogram_filter/histogram_filter.py new file mode 100644 index 00000000..253cbea9 --- /dev/null +++ b/Localization/histogram_filter/histogram_filter.py @@ -0,0 +1,258 @@ +""" + +Histogram Filter 2D localization example + + +In this simulation, x,y are unknown, yaw is known. + +Initial position is not needed. + +author: Atsushi Sakai (@Atsushi_twi) + +""" + +import math +import numpy as np +import matplotlib.pyplot as plt +import copy +from scipy.stats import norm +from scipy.ndimage import gaussian_filter + +# Parameters +EXTEND_AREA = 10.0 # [m] grid map extention length +SIM_TIME = 50.0 # simulation time [s] +DT = 0.1 # time tick [s] +MAX_RANGE = 10.0 # maximum observation range +MOTION_STD = 1.0 # standard deviation for motion gaussian distribution +RANGE_STD = 3.0 # standard deviation for observation gaussian distribution + +# grid map param +XY_RESO = 0.5 # xy grid resolution +MINX = -15.0 +MINY = -5.0 +MAXX = 15.0 +MAXY = 25.0 + +# simulation paramters +NOISE_RANGE = 2.0 # [m] 1σ range noise parameter +NOISE_SPEED = 0.5 # [m/s] 1σ speed noise parameter + + +show_animation = True + + +class grid_map(): + + def __init__(self): + self.data = None + self.xyreso = None + self.minx = None + self.miny = None + self.maxx = None + self.maxx = None + self.xw = None + self.yw = None + self.dx = 0.0 # movement distance + self.dy = 0.0 # movement distance + + +def histogram_filter_localization(gmap, u, z, yaw): + + gmap = motion_update(gmap, u, yaw) + + gmap = observation_update(gmap, z, RANGE_STD) + + return gmap + + +def calc_gaussian_observation_pdf(gmap, z, iz, ix, iy, std): + + # predicted range + x = ix * gmap.xyreso + gmap.minx + y = iy * gmap.xyreso + gmap.miny + d = math.sqrt((x - z[iz, 1])**2 + (y - z[iz, 2])**2) + + # likelihood + pdf = (1.0 - norm.cdf(abs(d - z[iz, 0]), 0.0, std)) + + return pdf + + +def observation_update(gmap, z, std): + + for iz in range(z.shape[0]): + for ix in range(gmap.xw): + for iy in range(gmap.yw): + gmap.data[ix][iy] *= calc_gaussian_observation_pdf( + gmap, z, iz, ix, iy, std) + + gmap = normalize_probability(gmap) + + return gmap + + +def calc_input(): + v = 1.0 # [m/s] + yawrate = 0.1 # [rad/s] + u = np.matrix([v, yawrate]).T + return u + + +def motion_model(x, u): + + F = np.matrix([[1.0, 0, 0, 0], + [0, 1.0, 0, 0], + [0, 0, 1.0, 0], + [0, 0, 0, 0]]) + + B = np.matrix([[DT * math.cos(x[2, 0]), 0], + [DT * math.sin(x[2, 0]), 0], + [0.0, DT], + [1.0, 0.0]]) + + x = F * x + B * u + + return x + + +def draw_heatmap(data, mx, my): + maxp = max([max(igmap) for igmap in data]) + plt.pcolor(mx, my, data, vmax=maxp, cmap=plt.cm.Blues) + plt.axis("equal") + + +def observation(xTrue, u, RFID): + + xTrue = motion_model(xTrue, u) + + z = np.matrix(np.zeros((0, 3))) + + for i in range(len(RFID[:, 0])): + + dx = xTrue[0, 0] - RFID[i, 0] + dy = xTrue[1, 0] - RFID[i, 1] + d = math.sqrt(dx**2 + dy**2) + if d <= MAX_RANGE: + # add noise to range observation + dn = d + np.random.randn() * NOISE_RANGE + zi = np.matrix([dn, RFID[i, 0], RFID[i, 1]]) + z = np.vstack((z, zi)) + + # add noise to speed + ud = u[:, :] + ud[0] += np.random.randn() * NOISE_SPEED + + return xTrue, z, ud + + +def normalize_probability(gmap): + + sump = sum([sum(igmap) for igmap in gmap.data]) + + for ix in range(gmap.xw): + for iy in range(gmap.yw): + gmap.data[ix][iy] /= sump + + return gmap + + +def init_gmap(xyreso, minx, miny, maxx, maxy): + + gmap = grid_map() + + gmap.xyreso = xyreso + gmap.minx = minx + gmap.miny = miny + gmap.maxx = maxx + gmap.maxy = maxy + gmap.xw = int(round((gmap.maxx - gmap.minx) / gmap.xyreso)) + gmap.yw = int(round((gmap.maxy - gmap.miny) / gmap.xyreso)) + + gmap.data = [[1.0 for i in range(gmap.yw)] for i in range(gmap.xw)] + gmap = normalize_probability(gmap) + + return gmap + + +def map_shift(gmap, xshift, yshift): + + tgmap = copy.deepcopy(gmap.data) + + for ix in range(gmap.xw): + for iy in range(gmap.yw): + nix = ix + xshift + niy = iy + yshift + + if nix >= 0 and nix < gmap.xw and niy >= 0 and niy < gmap.yw: + gmap.data[ix + xshift][iy + yshift] = tgmap[ix][iy] + + return gmap + + +def motion_update(gmap, u, yaw): + + gmap.dx += DT * math.cos(yaw) * u[0] + gmap.dy += DT * math.sin(yaw) * u[0] + + xshift = gmap.dx // gmap.xyreso + yshift = gmap.dy // gmap.xyreso + + if abs(xshift) >= 1.0 or abs(yshift) >= 1.0: # map should be shifted + gmap = map_shift(gmap, int(xshift), int(yshift)) + gmap.dx -= xshift * gmap.xyreso + gmap.dy -= yshift * gmap.xyreso + + gmap.data = gaussian_filter(gmap.data, sigma=MOTION_STD) + + return gmap + + +def calc_grid_index(gmap): + mx, my = np.mgrid[slice(gmap.minx - gmap.xyreso / 2.0, gmap.maxx + gmap.xyreso / 2.0, gmap.xyreso), + slice(gmap.miny - gmap.xyreso / 2.0, gmap.maxy + gmap.xyreso / 2.0, gmap.xyreso)] + + return mx, my + + +def main(): + print(__file__ + " start!!") + + # RFID positions [x, y] + RFID = np.array([[10.0, 0.0], + [10.0, 10.0], + [0.0, 15.0], + [-5.0, 20.0]]) + + time = 0.0 + + xTrue = np.matrix(np.zeros((4, 1))) + gmap = init_gmap(XY_RESO, MINX, MINY, MAXX, MAXY) + mx, my = calc_grid_index(gmap) # for grid map visualization + + while SIM_TIME >= time: + time += DT + print("Time:", time) + + u = calc_input() + + yaw = xTrue[2, 0] # Orientation is known + xTrue, z, ud = observation(xTrue, u, RFID) + + gmap = histogram_filter_localization(gmap, u, z, yaw) + + if show_animation: + plt.cla() + draw_heatmap(gmap.data, mx, my) + plt.plot(xTrue[0, :], xTrue[1, :], "xr") + plt.plot(RFID[:, 0], RFID[:, 1], ".k") + for i in range(z.shape[0]): + plt.plot([xTrue[0, :], z[i, 1]], [ + xTrue[1, :], z[i, 2]], "-k") + plt.title("Time[s]:" + str(time)[0: 4]) + plt.pause(0.1) + + print("Done") + + +if __name__ == '__main__': + main() diff --git a/Mapping/circle_fitting/animation.gif b/Mapping/circle_fitting/animation.gif new file mode 100644 index 00000000..9663fed5 Binary files /dev/null and b/Mapping/circle_fitting/animation.gif differ diff --git a/Mapping/circle_fitting/circle_fitting.py b/Mapping/circle_fitting/circle_fitting.py new file mode 100644 index 00000000..37799b03 --- /dev/null +++ b/Mapping/circle_fitting/circle_fitting.py @@ -0,0 +1,138 @@ +""" + +Object shape recognition with circle fitting + +author: Atsushi Sakai (@Atsushi_twi) + +""" + +import matplotlib.pyplot as plt +import math +import random +import numpy as np + +show_animation = True + + +def circle_fitting(x, y): + """ + Circle Fitting with least squared + input: point x-y positions + output cxe x center position + cye y center position + re radius of circle + error: prediction error + """ + + sumx = sum(x) + sumy = sum(y) + sumx2 = sum([ix ** 2 for ix in x]) + sumy2 = sum([iy ** 2 for iy in y]) + sumxy = sum([ix * iy for (ix, iy) in zip(x, y)]) + + F = np.array([[sumx2, sumxy, sumx], + [sumxy, sumy2, sumy], + [sumx, sumy, len(x)]]) + + G = np.array([[-sum([ix ** 3 + ix * iy ** 2 for (ix, iy) in zip(x, y)])], + [-sum([ix ** 2 * iy + iy ** 3 for (ix, iy) in zip(x, y)])], + [-sum([ix ** 2 + iy ** 2 for (ix, iy) in zip(x, y)])]]) + + T = np.linalg.inv(F).dot(G) + + cxe = float(T[0] / -2) + cye = float(T[1] / -2) + re = math.sqrt(cxe**2 + cye**2 - T[2]) + + error = sum([np.hypot(cxe - ix, cye - iy) - re for (ix, iy) in zip(x, y)]) + + return (cxe, cye, re, error) + + +def get_sample_points(cx, cy, cr, angle_reso): + x, y, angle, r = [], [], [], [] + + # points sampling + for theta in np.arange(0.0, 2.0 * math.pi, angle_reso): + nx = cx + cr * math.cos(theta) + ny = cy + cr * math.sin(theta) + nangle = math.atan2(ny, nx) + nr = math.hypot(nx, ny) * random.uniform(0.95, 1.05) + + x.append(nx) + y.append(ny) + angle.append(nangle) + r.append(nr) + + # ray casting filter + rx, ry = ray_casting_filter(x, y, angle, r, angle_reso) + + return rx, ry + + +def ray_casting_filter(xl, yl, thetal, rangel, angle_reso): + rx, ry = [], [] + rangedb = [float("inf") for _ in range( + int(math.floor((math.pi * 2.0) / angle_reso)) + 1)] + + for i in range(len(thetal)): + angleid = math.floor(thetal[i] / angle_reso) + + if rangedb[angleid] > rangel[i]: + rangedb[angleid] = rangel[i] + + for i in range(len(rangedb)): + t = i * angle_reso + if rangedb[i] != float("inf"): + rx.append(rangedb[i] * math.cos(t)) + ry.append(rangedb[i] * math.sin(t)) + + return rx, ry + + +def plot_circle(x, y, size, color="-b"): + deg = list(range(0, 360, 5)) + deg.append(0) + xl = [x + size * math.cos(math.radians(d)) for d in deg] + yl = [y + size * math.sin(math.radians(d)) for d in deg] + plt.plot(xl, yl, color) + + +def main(): + + # simulation parameters + simtime = 15.0 # simulation time + dt = 1.0 # time tick + + cx = -2.0 # initial x position of obstacle + cy = -8.0 # initial y position of obstacle + cr = 1.0 # obstacle radious + theta = math.radians(30.0) # obstacle moving direction + angle_reso = math.radians(3.0) # sensor angle resolution + + time = 0.0 + while time <= simtime: + time += dt + + cx += math.cos(theta) + cy += math.cos(theta) + + x, y = get_sample_points(cx, cy, cr, angle_reso) + + ex, ey, er, error = circle_fitting(x, y) + print("Error:", error) + + if show_animation: + plt.cla() + plt.axis("equal") + plt.plot(0.0, 0.0, "*r") + plot_circle(cx, cy, cr) + plt.plot(x, y, "xr") + plot_circle(ex, ey, er, "-r") + plt.pause(dt) + + print("Done") + + +if __name__ == '__main__': + main() diff --git a/Mapping/kmeans_clustering/animation.gif b/Mapping/kmeans_clustering/animation.gif new file mode 100644 index 00000000..4bc56826 Binary files /dev/null and b/Mapping/kmeans_clustering/animation.gif differ diff --git a/Mapping/kmeans_clustering/kmeans_clustering.py b/Mapping/kmeans_clustering/kmeans_clustering.py new file mode 100644 index 00000000..5d29aa11 --- /dev/null +++ b/Mapping/kmeans_clustering/kmeans_clustering.py @@ -0,0 +1,179 @@ +""" + +Object clustering with k-means algorithm + +author: Atsushi Sakai (@Atsushi_twi) + +""" + +import numpy as np +import math +import matplotlib.pyplot as plt +import random + +show_animation = True + + +class Clusters: + + def __init__(self, x, y, nlabel): + self.x = x + self.y = y + self.ndata = len(self.x) + self.nlabel = nlabel + self.labels = [random.randint(0, nlabel - 1) + for _ in range(self.ndata)] + self.cx = [0.0 for _ in range(nlabel)] + self.cy = [0.0 for _ in range(nlabel)] + + +def kmeans_clustering(rx, ry, nc): + + clusters = Clusters(rx, ry, nc) + clusters = calc_centroid(clusters) + + MAX_LOOP = 10 + DCOST_TH = 0.1 + pcost = 100.0 + for loop in range(MAX_LOOP): + # print("Loop:", loop) + clusters, cost = update_clusters(clusters) + clusters = calc_centroid(clusters) + + dcost = abs(cost - pcost) + if dcost < DCOST_TH: + break + pcost = cost + + return clusters + + +def calc_centroid(clusters): + + for ic in range(clusters.nlabel): + x, y = calc_labeled_points(ic, clusters) + ndata = len(x) + clusters.cx[ic] = sum(x) / ndata + clusters.cy[ic] = sum(y) / ndata + + return clusters + + +def update_clusters(clusters): + cost = 0.0 + + for ip in range(clusters.ndata): + px = clusters.x[ip] + py = clusters.y[ip] + + dx = [icx - px for icx in clusters.cx] + dy = [icy - py for icy in clusters.cy] + + dlist = [math.sqrt(idx**2 + idy**2) for (idx, idy) in zip(dx, dy)] + mind = min(dlist) + min_id = dlist.index(mind) + clusters.labels[ip] = min_id + cost += min_id + + return clusters, cost + + +def calc_labeled_points(ic, clusters): + + inds = np.array([i for i in range(clusters.ndata) + if clusters.labels[i] == ic]) + tx = np.array(clusters.x) + ty = np.array(clusters.y) + + x = tx[inds] + y = ty[inds] + + return x, y + + +def calc_raw_data(cx, cy, npoints, rand_d): + + rx, ry = [], [] + + for (icx, icy) in zip(cx, cy): + for _ in range(npoints): + rx.append(icx + rand_d * (random.random() - 0.5)) + ry.append(icy + rand_d * (random.random() - 0.5)) + + return rx, ry + + +def update_positions(cx, cy): + + DX1 = 0.4 + DY1 = 0.5 + + cx[0] += DX1 + cy[0] += DY1 + + DX2 = -0.3 + DY2 = -0.5 + + cx[1] += DX2 + cy[1] += DY2 + + return cx, cy + + +def calc_association(cx, cy, clusters): + + inds = [] + + for ic in range(len(cx)): + tcx = cx[ic] + tcy = cy[ic] + + dx = [icx - tcx for icx in clusters.cx] + dy = [icy - tcy for icy in clusters.cy] + + dlist = [math.sqrt(idx**2 + idy**2) for (idx, idy) in zip(dx, dy)] + min_id = dlist.index(min(dlist)) + inds.append(min_id) + + return inds + + +def main(): + print(__file__ + " start!!") + + cx = [0.0, 8.0] + cy = [0.0, 8.0] + npoints = 10 + rand_d = 3.0 + ncluster = 2 + sim_time = 15.0 + dt = 1.0 + time = 0.0 + + while time <= sim_time: + print("Time:", time) + time += dt + + # simulate objects + cx, cy = update_positions(cx, cy) + rx, ry = calc_raw_data(cx, cy, npoints, rand_d) + + clusters = kmeans_clustering(rx, ry, ncluster) + + # for animation + if show_animation: + plt.cla() + inds = calc_association(cx, cy, clusters) + for ic in inds: + x, y = calc_labeled_points(ic, clusters) + plt.plot(x, y, "x") + plt.plot(cx, cy, "o") + plt.xlim(-2.0, 10.0) + plt.ylim(-2.0, 10.0) + plt.pause(dt) + + print("Done") + + +if __name__ == '__main__': + main() diff --git a/Mapping/rectangle_fitting/rectangle_fitting.py b/Mapping/rectangle_fitting/rectangle_fitting.py new file mode 100644 index 00000000..07811009 --- /dev/null +++ b/Mapping/rectangle_fitting/rectangle_fitting.py @@ -0,0 +1,138 @@ +""" + +Object shape recognition with rectangle fitting + +author: Atsushi Sakai (@Atsushi_twi) + +""" + +import matplotlib.pyplot as plt +import math +import random +import numpy as np + +show_animation = True + + +def circle_fitting(x, y): + """ + Circle Fitting with least squared + input: point x-y positions + output cxe x center position + cye y center position + re radius of circle + error: prediction error + """ + + sumx = sum(x) + sumy = sum(y) + sumx2 = sum([ix ** 2 for ix in x]) + sumy2 = sum([iy ** 2 for iy in y]) + sumxy = sum([ix * iy for (ix, iy) in zip(x, y)]) + + F = np.array([[sumx2, sumxy, sumx], + [sumxy, sumy2, sumy], + [sumx, sumy, len(x)]]) + + G = np.array([[-sum([ix ** 3 + ix * iy ** 2 for (ix, iy) in zip(x, y)])], + [-sum([ix ** 2 * iy + iy ** 3 for (ix, iy) in zip(x, y)])], + [-sum([ix ** 2 + iy ** 2 for (ix, iy) in zip(x, y)])]]) + + T = np.linalg.inv(F).dot(G) + + cxe = float(T[0] / -2) + cye = float(T[1] / -2) + re = math.sqrt(cxe**2 + cye**2 - T[2]) + + error = sum([np.hypot(cxe - ix, cye - iy) - re for (ix, iy) in zip(x, y)]) + + return (cxe, cye, re, error) + + +def get_sample_points(cx, cy, cr, angle_reso): + x, y, angle, r = [], [], [], [] + + # points sampling + for theta in np.arange(0.0, 2.0 * math.pi, angle_reso): + nx = cx + cr * math.cos(theta) + ny = cy + cr * math.sin(theta) + nangle = math.atan2(ny, nx) + nr = math.hypot(nx, ny) * random.uniform(0.95, 1.05) + + x.append(nx) + y.append(ny) + angle.append(nangle) + r.append(nr) + + # ray casting filter + rx, ry = ray_casting_filter(x, y, angle, r, angle_reso) + + return rx, ry + + +def ray_casting_filter(xl, yl, thetal, rangel, angle_reso): + rx, ry = [], [] + rangedb = [float("inf") for _ in range( + int(math.floor((math.pi * 2.0) / angle_reso)) + 1)] + + for i in range(len(thetal)): + angleid = math.floor(thetal[i] / angle_reso) + + if rangedb[angleid] > rangel[i]: + rangedb[angleid] = rangel[i] + + for i in range(len(rangedb)): + t = i * angle_reso + if rangedb[i] != float("inf"): + rx.append(rangedb[i] * math.cos(t)) + ry.append(rangedb[i] * math.sin(t)) + + return rx, ry + + +def plot_circle(x, y, size, color="-b"): + deg = list(range(0, 360, 5)) + deg.append(0) + xl = [x + size * math.cos(math.radians(d)) for d in deg] + yl = [y + size * math.sin(math.radians(d)) for d in deg] + plt.plot(xl, yl, color) + + +def main(): + + # simulation parameters + simtime = 15.0 # simulation time + dt = 1.0 # time tick + + cx = -2.0 # initial x position of obstacle + cy = -8.0 # initial y position of obstacle + cr = 1.0 # obstacle radious + theta = math.radians(30.0) # obstacle moving direction + angle_reso = math.radians(3.0) # sensor angle resolution + + time = 0.0 + while time <= simtime: + time += dt + + cx += math.cos(theta) + cy += math.cos(theta) + + x, y = get_sample_points(cx, cy, cr, angle_reso) + + ex, ey, er, error = circle_fitting(x, y) + print("Error:", error) + + if show_animation: + plt.cla() + plt.axis("equal") + plt.plot(0.0, 0.0, "*r") + plot_circle(cx, cy, cr) + plt.plot(x, y, "xr") + plot_circle(ex, ey, er, "-r") + plt.pause(dt) + + print("Done") + + +if __name__ == '__main__': + main() diff --git a/PathPlanning/AStar/a_star.py b/PathPlanning/AStar/a_star.py index d26e36ab..1d4a186a 100644 --- a/PathPlanning/AStar/a_star.py +++ b/PathPlanning/AStar/a_star.py @@ -1,7 +1,12 @@ """ + A* grid based planning author: Atsushi Sakai(@Atsushi_twi) + Nikos Kanargias (nkana@tee.gr) + +See Wikipedia article (https://en.wikipedia.org/wiki/A*_search_algorithm) + """ import matplotlib.pyplot as plt @@ -59,9 +64,8 @@ def a_star_planning(sx, sy, gx, gy, ox, oy, reso, rr): while 1: c_id = min( - openset, key=lambda o: openset[o].cost + calc_h(ngoal, openset[o].x, openset[o].y)) + openset, key=lambda o: openset[o].cost + calc_heuristic(ngoal, openset[o])) current = openset[c_id] - # print("current", current) # show graph if show_animation: @@ -82,31 +86,32 @@ def a_star_planning(sx, sy, gx, gy, ox, oy, reso, rr): # expand search grid based on motion model for i in range(len(motion)): - node = Node(current.x + motion[i][0], current.y + motion[i][1], + node = Node(current.x + motion[i][0], + current.y + motion[i][1], current.cost + motion[i][2], c_id) n_id = calc_index(node, xw, minx, miny) + if n_id in closedset: + continue + if not verify_node(node, obmap, minx, miny, maxx, maxy): continue - if n_id in closedset: - continue - # Otherwise if it is already in the open set - if n_id in openset: - if openset[n_id].cost > node.cost: - openset[n_id].cost = node.cost - openset[n_id].pind = c_id + if n_id not in openset: + openset[n_id] = node # Discover a new node else: - openset[n_id] = node + if openset[n_id].cost >= node.cost: + # This path is the best until now. record it! + openset[n_id] = node rx, ry = calc_fianl_path(ngoal, closedset, reso) return rx, ry -def calc_h(ngoal, x, y): - w = 10.0 # weight of heuristic - d = w * math.sqrt((ngoal.x - x)**2 + (ngoal.y - y)**2) +def calc_heuristic(n1, n2): + w = 1.0 # weight of heuristic + d = w * math.sqrt((n1.x - n2.x)**2 + (n1.y - n2.y)**2) return d diff --git a/PathPlanning/AStar/animation.gif b/PathPlanning/AStar/animation.gif index 7d45520c..0cad36c4 100644 Binary files a/PathPlanning/AStar/animation.gif and b/PathPlanning/AStar/animation.gif differ diff --git a/PathPlanning/BatchInformedRRTStar/animation.gif b/PathPlanning/BatchInformedRRTStar/animation.gif new file mode 100644 index 00000000..937a7920 Binary files /dev/null and b/PathPlanning/BatchInformedRRTStar/animation.gif differ diff --git a/PathPlanning/BatchInformedRRTStar/batch_informed_rrtstar.py b/PathPlanning/BatchInformedRRTStar/batch_informed_rrtstar.py new file mode 100644 index 00000000..05bd254b --- /dev/null +++ b/PathPlanning/BatchInformedRRTStar/batch_informed_rrtstar.py @@ -0,0 +1,598 @@ +""" +Batch Informed Trees based path planning: +Uses a heuristic to efficiently search increasingly dense +RGGs while reusing previous information. Provides faster +convergence that RRT*, Informed RRT* and other sampling based +methods. + +Uses lazy connecting by combining sampling based methods and A* +like incremental graph search algorithms. + +author: Karan Chawla(@karanchawla) + Atsushi Sakai(@Atsushi_twi) + +Reference: https://arxiv.org/abs/1405.5848 +""" + +import random +import numpy as np +import math +import matplotlib.pyplot as plt + +show_animation = True + + +class RTree(object): + # Class to represent the explicit tree created + # while sampling through the state space + + def __init__(self, start=[0, 0], lowerLimit=[0, 0], upperLimit=[10, 10], resolution=1): + + self.vertices = dict() + self.edges = [] + self.start = start + self.lowerLimit = lowerLimit + self.upperLimit = upperLimit + self.dimension = len(lowerLimit) + self.num_cells = [0] * self.dimension + self.resolution = resolution + # compute the number of grid cells based on the limits and + # resolution given + for idx in range(self.dimension): + self.num_cells[idx] = np.ceil( + (upperLimit[idx] - lowerLimit[idx]) / resolution) + + vertex_id = self.realWorldToNodeId(start) + self.vertices[vertex_id] = [] + + def getRootId(self): + # return the id of the root of the tree + return 0 + + def addVertex(self, vertex): + # add a vertex to the tree + vertex_id = self.realWorldToNodeId(vertex) + self.vertices[vertex_id] = [] + return vertex_id + + def addEdge(self, v, x): + # create an edge between v and x vertices + if (v, x) not in self.edges: + self.edges.append((v, x)) + # since the tree is undirected + self.vertices[v].append(x) + self.vertices[x].append(v) + + def realCoordsToGridCoord(self, real_coord): + # convert real world coordinates to grid space + # depends on the resolution of the grid + # the output is the same as real world coords if the resolution + # is set to 1 + coord = [0] * self.dimension + for i in range(len(coord)): + start = self.lowerLimit[i] # start of the grid space + coord[i] = np.around((real_coord[i] - start) / self.resolution) + return coord + + def gridCoordinateToNodeId(self, coord): + # This function maps a grid coordinate to a unique + # node id + nodeId = 0 + for i in range(len(coord) - 1, -1, -1): + product = 1 + for j in range(0, i): + product = product * self.num_cells[j] + nodeId = nodeId + coord[i] * product + return nodeId + + def realWorldToNodeId(self, real_coord): + # first convert the given coordinates to grid space and then + # convert the grid space coordinates to a unique node id + return self.gridCoordinateToNodeId(self.realCoordsToGridCoord(real_coord)) + + def gridCoordToRealWorldCoord(self, coord): + # This function smaps a grid coordinate in discrete space + # to a configuration in the full configuration space + config = [0] * self.dimension + for i in range(0, len(coord)): + # start of the real world / configuration space + start = self.lowerLimit[i] + # step from the coordinate in the grid + grid_step = self.resolution * coord[i] + config[i] = start + grid_step + return config + + def nodeIdToGridCoord(self, node_id): + # This function maps a node id to the associated + # grid coordinate + coord = [0] * len(self.lowerLimit) + for i in range(len(coord) - 1, -1, -1): + # Get the product of the grid space maximums + prod = 1 + for j in range(0, i): + prod = prod * self.num_cells[j] + coord[i] = np.floor(node_id / prod) + node_id = node_id - (coord[i] * prod) + return coord + + def nodeIdToRealWorldCoord(self, nid): + # This function maps a node in discrete space to a configuraiton + # in the full configuration space + return self.gridCoordToRealWorldCoord(self.nodeIdToGridCoord(nid)) + +# Uses Batch Informed Trees to find a path from start to goal + + +class BITStar(object): + + def __init__(self, start, goal, + obstacleList, randArea, eta=2.0, + maxIter=80): + self.start = start + self.goal = goal + + self.minrand = randArea[0] + self.maxrand = randArea[1] + self.maxIter = maxIter + self.obstacleList = obstacleList + + self.vertex_queue = [] + self.edge_queue = [] + self.samples = dict() + self.g_scores = dict() + self.f_scores = dict() + self.nodes = dict() + self.r = float('inf') + self.eta = eta # tunable parameter + self.unit_ball_measure = 1 + self.old_vertices = [] + + # initialize tree + lowerLimit = [randArea[0], randArea[0]] + upperLimit = [randArea[1], randArea[1]] + self.tree = RTree(start=start, lowerLimit=lowerLimit, + upperLimit=upperLimit, resolution=0.01) + + def setup_planning(self): + self.startId = self.tree.realWorldToNodeId(self.start) + self.goalId = self.tree.realWorldToNodeId(self.goal) + + # add goal to the samples + self.samples[self.goalId] = self.goal + self.g_scores[self.goalId] = float('inf') + self.f_scores[self.goalId] = 0 + + # add the start id to the tree + self.tree.addVertex(self.start) + self.g_scores[self.startId] = 0 + self.f_scores[self.startId] = self.computeHeuristicCost( + self.startId, self.goalId) + + # max length we expect to find in our 'informed' sample space, starts as infinite + cBest = self.g_scores[self.goalId] + + # Computing the sampling space + cMin = math.sqrt(pow(self.start[0] - self.goal[1], 2) + + pow(self.start[0] - self.goal[1], 2)) / 1.5 + xCenter = np.matrix([[(self.start[0] + self.goal[0]) / 2.0], + [(self.goal[1] - self.start[1]) / 2.0], [0]]) + a1 = np.matrix([[(self.goal[0] - self.start[0]) / cMin], + [(self.goal[1] - self.start[1]) / cMin], [0]]) + etheta = math.atan2(a1[1], a1[0]) + # first column of idenity matrix transposed + id1_t = np.matrix([1.0, 0.0, 0.0]) + M = np.dot(a1, id1_t) + U, S, Vh = np.linalg.svd(M, 1, 1) + C = np.dot(np.dot(U, np.diag( + [1.0, 1.0, np.linalg.det(U) * np.linalg.det(np.transpose(Vh))])), Vh) + + self.samples.update(self.informedSample( + 200, cBest, cMin, xCenter, C)) + + return etheta, cMin, xCenter, C, cBest + + def setup_sample(self, iterations, foundGoal, cMin, xCenter, C, cBest): + + if len(self.vertex_queue) == 0 and len(self.edge_queue) == 0: + print("Batch: ", iterations) + # Using informed rrt star way of computing the samples + self.r = 2.0 + if iterations != 0: + if foundGoal: + # a better way to do this would be to make number of samples + # a function of cMin + m = 200 + self.samples = dict() + self.samples[self.goalId] = self.goal + else: + m = 100 + cBest = self.g_scores[self.goalId] + self.samples.update(self.informedSample( + m, cBest, cMin, xCenter, C)) + + # make the old vertices the new vertices + self.old_vertices += self.tree.vertices.keys() + # add the vertices to the vertex queue + for nid in self.tree.vertices.keys(): + if nid not in self.vertex_queue: + self.vertex_queue.append(nid) + return cBest + + def plan(self, animation=True): + + etheta, cMin, xCenter, C, cBest = self.setup_planning() + iterations = 0 + + foundGoal = False + # run until done + while (iterations < self.maxIter): + cBest = self.setup_sample(iterations, + foundGoal, cMin, xCenter, C, cBest) + # expand the best vertices until an edge is better than the vertex + # this is done because the vertex cost represents the lower bound + # on the edge cost + while(self.bestVertexQueueValue() <= self.bestEdgeQueueValue()): + self.expandVertex(self.bestInVertexQueue()) + + # add the best edge to the tree + bestEdge = self.bestInEdgeQueue() + self.edge_queue.remove(bestEdge) + + # Check if this can improve the current solution + estimatedCostOfVertex = self.g_scores[bestEdge[0]] + self.computeDistanceCost( + bestEdge[0], bestEdge[1]) + self.computeHeuristicCost(bestEdge[1], self.goalId) + estimatedCostOfEdge = self.computeDistanceCost(self.startId, bestEdge[0]) + self.computeHeuristicCost( + bestEdge[0], bestEdge[1]) + self.computeHeuristicCost(bestEdge[1], self.goalId) + actualCostOfEdge = self.g_scores[bestEdge[0]] + \ + self.computeDistanceCost(bestEdge[0], bestEdge[1]) + + f1 = estimatedCostOfVertex < self.g_scores[self.goalId] + f2 = estimatedCostOfEdge < self.g_scores[self.goalId] + f3 = actualCostOfEdge < self.g_scores[self.goalId] + + if f1 and f2 and f3: + # connect this edge + firstCoord = self.tree.nodeIdToRealWorldCoord( + bestEdge[0]) + secondCoord = self.tree.nodeIdToRealWorldCoord( + bestEdge[1]) + path = self.connect(firstCoord, secondCoord) + lastEdge = self.tree.realWorldToNodeId(secondCoord) + if path is None or len(path) == 0: + continue + nextCoord = path[len(path) - 1, :] + nextCoordPathId = self.tree.realWorldToNodeId( + nextCoord) + bestEdge = (bestEdge[0], nextCoordPathId) + if(bestEdge[1] in self.tree.vertices.keys()): + continue + else: + try: + del self.samples[bestEdge[1]] + except(KeyError): + pass + eid = self.tree.addVertex(nextCoord) + self.vertex_queue.append(eid) + if eid == self.goalId or bestEdge[0] == self.goalId or bestEdge[1] == self.goalId: + print("Goal found") + foundGoal = True + + self.tree.addEdge(bestEdge[0], bestEdge[1]) + + g_score = self.computeDistanceCost( + bestEdge[0], bestEdge[1]) + self.g_scores[bestEdge[1]] = g_score + \ + self.g_scores[bestEdge[0]] + self.f_scores[bestEdge[1]] = g_score + \ + self.computeHeuristicCost(bestEdge[1], self.goalId) + self.updateGraph() + + # visualize new edge + if animation: + self.drawGraph(xCenter=xCenter, cBest=cBest, + cMin=cMin, etheta=etheta, samples=self.samples.values(), + start=firstCoord, end=secondCoord, tree=self.tree.edges) + + self.remove_queue(lastEdge, bestEdge) + + else: + print("Nothing good") + self.edge_queue = [] + self.vertex_queue = [] + + iterations += 1 + + print("Finding the path") + return self.find_final_path() + + def find_final_path(self): + plan = [] + plan.append(self.goal) + currId = self.goalId + while (currId != self.startId): + plan.append(self.tree.nodeIdToRealWorldCoord(currId)) + try: + currId = self.nodes[currId] + except(KeyError): + print("Path key error") + return [] + + plan.append(self.start) + plan = plan[::-1] # reverse the plan + + return plan + + def remove_queue(self, lastEdge, bestEdge): + for edge in self.edge_queue: + if(edge[1] == bestEdge[1]): + if self.g_scores[edge[1]] + self.computeDistanceCost(edge[1], bestEdge[1]) >= self.g_scores[self.goalId]: + if(lastEdge, bestEdge[1]) in self.edge_queue: + self.edge_queue.remove( + (lastEdge, bestEdge[1])) + + def connect(self, start, end): + # A function which attempts to extend from a start coordinates + # to goal coordinates + steps = int(self.computeDistanceCost(self.tree.realWorldToNodeId( + start), self.tree.realWorldToNodeId(end)) * 10) + x = np.linspace(start[0], end[0], num=steps) + y = np.linspace(start[1], end[1], num=steps) + for i in range(len(x)): + if(self._collisionCheck(x[i], y[i])): + if(i == 0): + return None + # if collision, send path until collision + return np.vstack((x[0:i], y[0:i])).transpose() + + return np.vstack((x, y)).transpose() + + def _collisionCheck(self, x, y): + for (ox, oy, size) in self.obstacleList: + dx = ox - x + dy = oy - y + d = dx * dx + dy * dy + if d <= size ** 2: + return True # collision + return False + + # def prune(self, c): + + def computeHeuristicCost(self, start_id, goal_id): + # Using Manhattan distance as heuristic + start = np.array(self.tree.nodeIdToRealWorldCoord(start_id)) + goal = np.array(self.tree.nodeIdToRealWorldCoord(goal_id)) + + return np.linalg.norm(start - goal, 2) + + def computeDistanceCost(self, vid, xid): + # L2 norm distance + start = np.array(self.tree.nodeIdToRealWorldCoord(vid)) + stop = np.array(self.tree.nodeIdToRealWorldCoord(xid)) + + return np.linalg.norm(stop - start, 2) + + # Sample free space confined in the radius of ball R + def informedSample(self, m, cMax, cMin, xCenter, C): + samples = dict() + print("g_Score goal id: ", self.g_scores[self.goalId]) + for i in range(m + 1): + if cMax < float('inf'): + r = [cMax / 2.0, + math.sqrt(cMax**2 - cMin**2) / 2.0, + math.sqrt(cMax**2 - cMin**2) / 2.0] + L = np.diag(r) + xBall = self.sampleUnitBall() + rnd = np.dot(np.dot(C, L), xBall) + xCenter + rnd = [rnd[(0, 0)], rnd[(1, 0)]] + random_id = self.tree.realWorldToNodeId(rnd) + samples[random_id] = rnd + else: + rnd = self.sampleFreeSpace() + random_id = self.tree.realWorldToNodeId(rnd) + samples[random_id] = rnd + return samples + + # Sample point in a unit ball + def sampleUnitBall(self): + a = random.random() + b = random.random() + + if b < a: + a, b = b, a + + sample = (b * math.cos(2 * math.pi * a / b), + b * math.sin(2 * math.pi * a / b)) + return np.array([[sample[0]], [sample[1]], [0]]) + + def sampleFreeSpace(self): + rnd = [random.uniform(self.minrand, self.maxrand), + random.uniform(self.minrand, self.maxrand)] + + return rnd + + def bestVertexQueueValue(self): + if(len(self.vertex_queue) == 0): + return float('inf') + values = [self.g_scores[v] + + self.computeHeuristicCost(v, self.goalId) for v in self.vertex_queue] + values.sort() + return values[0] + + def bestEdgeQueueValue(self): + if(len(self.edge_queue) == 0): + return float('inf') + # return the best value in the queue by score g_tau[v] + c(v,x) + h(x) + values = [self.g_scores[e[0]] + self.computeDistanceCost(e[0], e[1]) + + self.computeHeuristicCost(e[1], self.goalId) for e in self.edge_queue] + values.sort(reverse=True) + return values[0] + + def bestInVertexQueue(self): + # return the best value in the vertex queue + v_plus_vals = [(v, self.g_scores[v] + self.computeHeuristicCost(v, self.goalId)) + for v in self.vertex_queue] + v_plus_vals = sorted(v_plus_vals, key=lambda x: x[1]) + # print(v_plus_vals) + return v_plus_vals[0][0] + + def bestInEdgeQueue(self): + e_and_values = [(e[0], e[1], self.g_scores[e[0]] + self.computeDistanceCost( + e[0], e[1]) + self.computeHeuristicCost(e[1], self.goalId)) for e in self.edge_queue] + e_and_values = sorted(e_and_values, key=lambda x: x[2]) + + return (e_and_values[0][0], e_and_values[0][1]) + + def expandVertex(self, vid): + self.vertex_queue.remove(vid) + + # get the coordinates for given vid + currCoord = np.array(self.tree.nodeIdToRealWorldCoord(vid)) + + # get the nearest value in vertex for every one in samples where difference is + # less than the radius + neigbors = [] + for sid, scoord in self.samples.items(): + scoord = np.array(scoord) + if(np.linalg.norm(scoord - currCoord, 2) <= self.r and sid != vid): + neigbors.append((sid, scoord)) + + # add an edge to the edge queue is the path might improve the solution + for neighbor in neigbors: + sid = neighbor[0] + scoord = neighbor[1] + estimated_f_score = self.computeDistanceCost( + self.startId, vid) + self.computeHeuristicCost(sid, self.goalId) + self.computeDistanceCost(vid, sid) + if estimated_f_score < self.g_scores[self.goalId]: + self.edge_queue.append((vid, sid)) + + # add the vertex to the edge queue + self.add_vertex_to_edge_queue(vid, currCoord) + + def add_vertex_to_edge_queue(self, vid, currCoord): + if vid not in self.old_vertices: + neigbors = [] + for v, edges in self.tree.vertices.items(): + if v != vid and (v, vid) not in self.edge_queue and (vid, v) not in self.edge_queue: + vcoord = self.tree.nodeIdToRealWorldCoord(v) + if(np.linalg.norm(vcoord - currCoord, 2) <= self.r and v != vid): + neigbors.append((vid, vcoord)) + + for neighbor in neigbors: + sid = neighbor[0] + estimated_f_score = self.computeDistanceCost(self.startId, vid) + \ + self.computeDistanceCost( + vid, sid) + self.computeHeuristicCost(sid, self.goalId) + if estimated_f_score < self.g_scores[self.goalId] and (self.g_scores[vid] + self.computeDistanceCost(vid, sid)) < self.g_scores[sid]: + self.edge_queue.append((vid, sid)) + + def updateGraph(self): + closedSet = [] + openSet = [] + currId = self.startId + openSet.append(currId) + + while len(openSet) != 0: + # get the element with lowest f_score + currId = min(openSet, key=lambda x: self.f_scores[x]) + + # remove element from open set + openSet.remove(currId) + + # Check if we're at the goal + if(currId == self.goalId): + self.nodes[self.goalId] + break + + if(currId not in closedSet): + closedSet.append(currId) + + # find a non visited successor to the current node + successors = self.tree.vertices[currId] + for succesor in successors: + if(succesor in closedSet): + continue + else: + # claculate tentative g score + g_score = self.g_scores[currId] + \ + self.computeDistanceCost(currId, succesor) + if succesor not in openSet: + # add the successor to open set + openSet.append(succesor) + elif g_score >= self.g_scores[succesor]: + continue + + # update g and f scores + self.g_scores[succesor] = g_score + self.f_scores[succesor] = g_score + \ + self.computeHeuristicCost(succesor, self.goalId) + + # store the parent and child + self.nodes[succesor] = currId + + def drawGraph(self, xCenter=None, cBest=None, cMin=None, etheta=None, + samples=None, start=None, end=None, tree=None): + print("Plotting Graph") + plt.clf() + for rnd in samples: + if rnd is not None: + plt.plot(rnd[0], rnd[1], "^k") + if cBest != float('inf'): + self.plot_ellipse(xCenter, cBest, cMin, etheta) + + if start is not None and end is not None: + plt.plot([start[0], start[1]], [end[0], end[1]], "-g") + + for (ox, oy, size) in self.obstacleList: + plt.plot(ox, oy, "ok", ms=30 * size) + + plt.plot(self.start[0], self.start[1], "xr") + plt.plot(self.goal[0], self.goal[1], "xr") + plt.axis([-2, 15, -2, 15]) + plt.grid(True) + plt.pause(0.01) + + def plot_ellipse(self, xCenter, cBest, cMin, etheta): + + a = math.sqrt(cBest**2 - cMin**2) / 2.0 + b = cBest / 2.0 + angle = math.pi / 2.0 - etheta + cx = xCenter[0] + cy = xCenter[1] + + t = np.arange(0, 2 * math.pi + 0.1, 0.1) + x = [a * math.cos(it) for it in t] + y = [b * math.sin(it) for it in t] + R = np.matrix([[math.cos(angle), math.sin(angle)], + [-math.sin(angle), math.cos(angle)]]) + fx = R * np.matrix([x, y]) + px = np.array(fx[0, :] + cx).flatten() + py = np.array(fx[1, :] + cy).flatten() + plt.plot(cx, cy, "xc") + plt.plot(px, py, "--c") + + +def main(): + print("Starting Batch Informed Trees Star planning") + obstacleList = [ + (5, 5, 0.5), + (9, 6, 1), + (7, 5, 1), + (1, 5, 1), + (3, 6, 1), + (7, 9, 1) + ] + + bitStar = BITStar(start=[-1, 0], goal=[3, 8], obstacleList=obstacleList, + randArea=[-2, 15]) + path = bitStar.plan(animation=show_animation) + print("Done") + + if show_animation: + plt.plot([x for (x, y) in path], [y for (x, y) in path], '-r') + plt.grid(True) + plt.pause(0.05) + plt.show() + + +if __name__ == '__main__': + main() diff --git a/PathPlanning/BezierPath/bezier_path.py b/PathPlanning/BezierPath/bezier_path.py index c12229d7..5188ad81 100644 --- a/PathPlanning/BezierPath/bezier_path.py +++ b/PathPlanning/BezierPath/bezier_path.py @@ -1,109 +1,203 @@ """ -Path Planning with 4 point Beizer curve +Path Planning with Bezier curve. author: Atsushi Sakai(@Atsushi_twi) """ +from __future__ import division, print_function import scipy.special import numpy as np import matplotlib.pyplot as plt -import math show_animation = True -def calc_4point_bezier_path(sx, sy, syaw, ex, ey, eyaw, offset): - D = math.sqrt((sx - ex)**2 + (sy - ey)**2) / offset - cp = np.array( +def calc_4points_bezier_path(sx, sy, syaw, ex, ey, eyaw, offset): + """ + Compute control points and path given start and end position. + + :param sx: (float) x-coordinate of the starting point + :param sy: (float) y-coordinate of the starting point + :param syaw: (float) yaw angle at start + :param ex: (float) x-coordinate of the ending point + :param ey: (float) y-coordinate of the ending point + :param eyaw: (float) yaw angle at the end + :param offset: (float) + :return: (numpy array, numpy array) + """ + dist = np.sqrt((sx - ex) ** 2 + (sy - ey) ** 2) / offset + control_points = np.array( [[sx, sy], - [sx + D * math.cos(syaw), sy + D * math.sin(syaw)], - [ex - D * math.cos(eyaw), ey - D * math.sin(eyaw)], + [sx + dist * np.cos(syaw), sy + dist * np.sin(syaw)], + [ex - dist * np.cos(eyaw), ey - dist * np.sin(eyaw)], [ex, ey]]) + path = calc_bezier_path(control_points, n_points=100) + + return path, control_points + + +def calc_bezier_path(control_points, n_points=100): + """ + Compute bezier path (trajectory) given control points. + + :param control_points: (numpy array) + :param n_points: (int) number of points in the trajectory + :return: (numpy array) + """ traj = [] - for t in np.linspace(0, 1, 100): - traj.append(bezier(3, t, cp)) - P = np.array(traj) + for t in np.linspace(0, 1, n_points): + traj.append(bezier(t, control_points)) - return P, cp + return np.array(traj) -def bernstein(n, i, t): - return scipy.special.comb(n, i) * t**i * (1 - t)**(n - i) +def bernstein_poly(n, i, t): + """ + Bernstein polynom. + + :param n: (int) polynom degree + :param i: (int) + :param t: (float) + :return: (float) + """ + return scipy.special.comb(n, i) * t ** i * (1 - t) ** (n - i) -def bezier(n, t, q): - p = np.zeros(2) - for i in range(n + 1): - p += bernstein(n, i, t) * q[i] - return p +def bezier(t, control_points): + """ + Return one point on the bezier curve. + + :param t: (float) number in [0, 1] + :param control_points: (numpy array) + :return: (numpy array) Coordinates of the point + """ + n = len(control_points) - 1 + return np.sum([bernstein_poly(n, i, t) * control_points[i] for i in range(n + 1)], axis=0) + + +def bezier_derivatives_control_points(control_points, n_derivatives): + """ + Compute control points of the successive derivatives of a given bezier curve. + + A derivative of a bezier curve is a bezier curve. + See https://pomax.github.io/bezierinfo/#derivatives + for detailed explanations + + :param control_points: (numpy array) + :param n_derivatives: (int) + e.g., n_derivatives=2 -> compute control points for first and second derivatives + :return: ([numpy array]) + """ + w = {0: control_points} + for i in range(n_derivatives): + n = len(w[i]) + w[i + 1] = np.array([(n - 1) * (w[i][j + 1] - w[i][j]) for j in range(n - 1)]) + return w + + +def curvature(dx, dy, ddx, ddy): + """ + Compute curvature at one point given first and second derivatives. + + :param dx: (float) First derivative along x axis + :param dy: (float) + :param ddx: (float) Second derivative along x axis + :param ddy: (float) + :return: (float) + """ + return (dx * ddy - dy * ddx) / (dx ** 2 + dy ** 2) ** (3 / 2) def plot_arrow(x, y, yaw, length=1.0, width=0.5, fc="r", ec="k"): - u""" - Plot arrow - """ - + """Plot arrow.""" if not isinstance(x, float): for (ix, iy, iyaw) in zip(x, y, yaw): plot_arrow(ix, iy, iyaw) else: - plt.arrow(x, y, length * math.cos(yaw), length * math.sin(yaw), + plt.arrow(x, y, length * np.cos(yaw), length * np.sin(yaw), fc=fc, ec=ec, head_width=width, head_length=width) plt.plot(x, y) def main(): + """Plot an example bezier curve.""" start_x = 10.0 # [m] start_y = 1.0 # [m] - start_yaw = math.radians(180.0) # [rad] + start_yaw = np.radians(180.0) # [rad] end_x = -0.0 # [m] end_y = -3.0 # [m] - end_yaw = math.radians(-45.0) # [rad] + end_yaw = np.radians(-45.0) # [rad] offset = 3.0 - P, cp = calc_4point_bezier_path( + path, control_points = calc_4points_bezier_path( start_x, start_y, start_yaw, end_x, end_y, end_yaw, offset) - assert P.T[0][0] == start_x, "path is invalid" - assert P.T[1][0] == start_y, "path is invalid" - assert P.T[0][-1] == end_x, "path is invalid" - assert P.T[1][-1] == end_y, "path is invalid" + # Note: alternatively, instead of specifying start and end position + # you can directly define n control points and compute the path: + # control_points = np.array([[5., 1.], [-2.78, 1.], [-11.5, -4.5], [-6., -8.]]) + # path = calc_bezier_path(control_points, n_points=100) + + # Display the tangent, normal and radius of cruvature at a given point + t = 0.86 # Number in [0, 1] + x_target, y_target = bezier(t, control_points) + derivatives_cp = bezier_derivatives_control_points(control_points, 2) + point = bezier(t, control_points) + dt = bezier(t, derivatives_cp[1]) + ddt = bezier(t, derivatives_cp[2]) + # Radius of curvature + radius = 1 / curvature(dt[0], dt[1], ddt[0], ddt[1]) + # Normalize derivative + dt /= np.linalg.norm(dt, 2) + tangent = np.array([point, point + dt]) + normal = np.array([point, point + [- dt[1], dt[0]]]) + curvature_center = point + np.array([- dt[1], dt[0]]) * radius + circle = plt.Circle(tuple(curvature_center), radius, color=(0, 0.8, 0.8), fill=False, linewidth=1) + + assert path.T[0][0] == start_x, "path is invalid" + assert path.T[1][0] == start_y, "path is invalid" + assert path.T[0][-1] == end_x, "path is invalid" + assert path.T[1][-1] == end_y, "path is invalid" if show_animation: - plt.plot(P.T[0], P.T[1], label="Bezier Path") - plt.plot(cp.T[0], cp.T[1], '--o', label="Control Points") + fig, ax = plt.subplots() + ax.plot(path.T[0], path.T[1], label="Bezier Path") + ax.plot(control_points.T[0], control_points.T[1], '--o', label="Control Points") + ax.plot(x_target, y_target) + ax.plot(tangent[:, 0], tangent[:, 1], label="Tangent") + ax.plot(normal[:, 0], normal[:, 1], label="Normal") + ax.add_artist(circle) plot_arrow(start_x, start_y, start_yaw) plot_arrow(end_x, end_y, end_yaw) - plt.legend() - plt.axis("equal") - plt.grid(True) + ax.legend() + ax.axis("equal") + ax.grid(True) plt.show() def main2(): + """Show the effect of the offset.""" start_x = 10.0 # [m] start_y = 1.0 # [m] - start_yaw = math.radians(180.0) # [rad] + start_yaw = np.radians(180.0) # [rad] end_x = -0.0 # [m] end_y = -3.0 # [m] - end_yaw = math.radians(-45.0) # [rad] - offset = 3.0 + end_yaw = np.radians(-45.0) # [rad] for offset in np.arange(1.0, 5.0, 1.0): - P, cp = calc_4point_bezier_path( + path, control_points = calc_4points_bezier_path( start_x, start_y, start_yaw, end_x, end_y, end_yaw, offset) - assert P.T[0][0] == start_x, "path is invalid" - assert P.T[1][0] == start_y, "path is invalid" - assert P.T[0][-1] == end_x, "path is invalid" - assert P.T[1][-1] == end_y, "path is invalid" + assert path.T[0][0] == start_x, "path is invalid" + assert path.T[1][0] == start_y, "path is invalid" + assert path.T[0][-1] == end_x, "path is invalid" + assert path.T[1][-1] == end_y, "path is invalid" if show_animation: - plt.plot(P.T[0], P.T[1], label="Offset=" + str(offset)) + plt.plot(path.T[0], path.T[1], label="Offset=" + str(offset)) if show_animation: plot_arrow(start_x, start_y, start_yaw) diff --git a/PathPlanning/ClosedLoopRRTStar/closed_loop_rrt_star_car.py b/PathPlanning/ClosedLoopRRTStar/closed_loop_rrt_star_car.py index f840330a..f97cf906 100644 --- a/PathPlanning/ClosedLoopRRTStar/closed_loop_rrt_star_car.py +++ b/PathPlanning/ClosedLoopRRTStar/closed_loop_rrt_star_car.py @@ -12,11 +12,12 @@ import random import math import copy import numpy as np -import reeds_shepp_path_planning import pure_pursuit -import unicycle_model import matplotlib.pyplot as plt +import reeds_shepp_path_planning +import unicycle_model + show_animation = True @@ -220,14 +221,10 @@ class RRT(): return newNode + def pi_2_pi(self, angle): - while(angle > math.pi): - angle = angle - 2.0 * math.pi + return (angle + math.pi) % (2*math.pi) - math.pi - while(angle < -math.pi): - angle = angle + 2.0 * math.pi - - return angle def steer(self, rnd, nind): # print(rnd) @@ -337,18 +334,14 @@ class RRT(): self.nodeList[i] = tNode def DrawGraph(self, rnd=None): - u""" + """ Draw Graph """ - # plt.clf() if rnd is not None: plt.plot(rnd.x, rnd.y, "^k") for node in self.nodeList: if node.parent is not None: plt.plot(node.path_x, node.path_y, "-g") - pass - # plt.plot([node.x, self.nodeList[node.parent].x], [ - # node.y, self.nodeList[node.parent].y], "-g") for (ox, oy, size) in self.obstacleList: plt.plot(ox, oy, "ok", ms=30 * size) @@ -362,9 +355,6 @@ class RRT(): plt.grid(True) plt.pause(0.01) - # plt.show() - # input() - def GetNearestListIndex(self, nodeList, rnd): dlist = [(node.x - rnd.x) ** 2 + (node.y - rnd.y) ** 2 + @@ -399,7 +389,7 @@ class RRT(): class Node(): - u""" + """ RRT Node """ @@ -417,14 +407,6 @@ class Node(): def main(): print("Start rrt start planning") # ====Search Path with RRT==== - obstacleList = [ - (5, 5, 1), - (3, 6, 2), - (3, 8, 2), - (3, 10, 2), - (7, 5, 2), - (9, 5, 2) - ] # [x,y,size(radius)] obstacleList = [ (5, 5, 1), (4, 6, 1), diff --git a/PathPlanning/ClosedLoopRRTStar/unicycle_model.py b/PathPlanning/ClosedLoopRRTStar/unicycle_model.py index f2a18ca9..5185eb1c 100644 --- a/PathPlanning/ClosedLoopRRTStar/unicycle_model.py +++ b/PathPlanning/ClosedLoopRRTStar/unicycle_model.py @@ -1,15 +1,15 @@ -#! /usr/bin/python -# -*- coding: utf-8 -*- """ +Unicycle model class author Atsushi Sakai + """ import math dt = 0.05 # [s] -L = 2.9 # [m] +L = 0.9 # [m] steer_max = math.radians(40.0) curvature_max = math.tan(steer_max) / L curvature_max = 1.0 / curvature_max + 1.0 @@ -38,13 +38,7 @@ def update(state, a, delta): def pi_2_pi(angle): - while(angle > math.pi): - angle = angle - 2.0 * math.pi - - while(angle < -math.pi): - angle = angle + 2.0 * math.pi - - return angle + return (angle + math.pi) % (2*math.pi) - math.pi if __name__ == '__main__': diff --git a/PathPlanning/CubicSpline/cubic_spline_planner.py b/PathPlanning/CubicSpline/cubic_spline_planner.py index d1df728c..6483a8bf 100644 --- a/PathPlanning/CubicSpline/cubic_spline_planner.py +++ b/PathPlanning/CubicSpline/cubic_spline_planner.py @@ -1,7 +1,7 @@ """ -cubic spline planner +Cubic spline planner -Author: Atsushi Sakai +Author: Atsushi Sakai(@Atsushi_twi) """ import math @@ -10,7 +10,7 @@ import bisect class Spline: - u""" + """ Cubic Spline class """ @@ -40,7 +40,7 @@ class Spline: self.b.append(tb) def calc(self, t): - u""" + """ Calc position if t is outside of the input x, return None @@ -60,7 +60,7 @@ class Spline: return result def calcd(self, t): - u""" + """ Calc first derivative if t is outside of the input x, return None @@ -77,7 +77,7 @@ class Spline: return result def calcdd(self, t): - u""" + """ Calc second derivative """ @@ -92,13 +92,13 @@ class Spline: return result def __search_index(self, x): - u""" + """ search data segment index """ return bisect.bisect(self.x, x) - 1 def __calc_A(self, h): - u""" + """ calc matrix A for spline coefficient c """ A = np.zeros((self.nx, self.nx)) @@ -116,19 +116,18 @@ class Spline: return A def __calc_B(self, h): - u""" + """ calc matrix B for spline coefficient c """ B = np.zeros(self.nx) for i in range(self.nx - 2): B[i + 1] = 3.0 * (self.a[i + 2] - self.a[i + 1]) / \ h[i + 1] - 3.0 * (self.a[i + 1] - self.a[i]) / h[i] - # print(B) return B class Spline2D: - u""" + """ 2D Cubic Spline class """ @@ -148,7 +147,7 @@ class Spline2D: return s def calc_position(self, s): - u""" + """ calc position """ x = self.sx.calc(s) @@ -157,18 +156,18 @@ class Spline2D: return x, y def calc_curvature(self, s): - u""" + """ calc curvature """ dx = self.sx.calcd(s) ddx = self.sx.calcdd(s) dy = self.sy.calcd(s) ddy = self.sy.calcdd(s) - k = (ddy * dx - ddx * dy) / (dx ** 2 + dy ** 2) + k = (ddy * dx - ddx * dy) / ((dx ** 2 + dy ** 2)**(3 / 2)) return k def calc_yaw(self, s): - u""" + """ calc yaw """ dx = self.sx.calcd(s) @@ -197,9 +196,10 @@ def main(): import matplotlib.pyplot as plt x = [-2.5, 0.0, 2.5, 5.0, 7.5, 3.0, -1.0] y = [0.7, -6, 5, 6.5, 0.0, 5.0, -2.0] + ds = 0.1 # [m] distance of each intepolated points sp = Spline2D(x, y) - s = np.arange(0, sp.s[-1], 0.1) + s = np.arange(0, sp.s[-1], ds) rx, ry, ryaw, rk = [], [], [], [] for i_s in s: diff --git a/PathPlanning/Dijkstra/dijkstra.py b/PathPlanning/Dijkstra/dijkstra.py index 2a511bfb..c1c865e1 100644 --- a/PathPlanning/Dijkstra/dijkstra.py +++ b/PathPlanning/Dijkstra/dijkstra.py @@ -85,12 +85,12 @@ def dijkstra_planning(sx, sy, gx, gy, ox, oy, reso, rr): else: openset[n_id] = node - rx, ry = calc_fianl_path(ngoal, closedset, reso) + rx, ry = calc_final_path(ngoal, closedset, reso) return rx, ry -def calc_fianl_path(ngoal, closedset, reso): +def calc_final_path(ngoal, closedset, reso): # generate final course rx, ry = [ngoal.x * reso], [ngoal.y * reso] pind = ngoal.pind diff --git a/PathPlanning/DubinsPath/dubins_path_planning.py b/PathPlanning/DubinsPath/dubins_path_planning.py index 52eb1c1a..4477cc13 100644 --- a/PathPlanning/DubinsPath/dubins_path_planning.py +++ b/PathPlanning/DubinsPath/dubins_path_planning.py @@ -17,13 +17,7 @@ def mod2pi(theta): def pi_2_pi(angle): - while(angle >= math.pi): - angle = angle - 2.0 * math.pi - - while(angle <= -math.pi): - angle = angle + 2.0 * math.pi - - return angle + return (angle + math.pi) % (2*math.pi) - math.pi def LSL(alpha, beta, d): diff --git a/PathPlanning/Eta3SplinePath/animation.gif b/PathPlanning/Eta3SplinePath/animation.gif new file mode 100644 index 00000000..92a3e06a Binary files /dev/null and b/PathPlanning/Eta3SplinePath/animation.gif differ diff --git a/PathPlanning/Eta3SplinePath/eta3_spline_path.py b/PathPlanning/Eta3SplinePath/eta3_spline_path.py new file mode 100644 index 00000000..64f64faa --- /dev/null +++ b/PathPlanning/Eta3SplinePath/eta3_spline_path.py @@ -0,0 +1,308 @@ +""" + +\eta^3 polynomials planner + +author: Joe Dinius, Ph.D (https://jwdinius.github.io) + Atsushi Sakai (@Atsushi_twi) + +Ref: + +- [\eta^3-Splines for the Smooth Path Generation of Wheeled Mobile Robots](https://ieeexplore.ieee.org/document/4339545/) + +""" + +import numpy as np +import matplotlib.pyplot as plt +from scipy.integrate import quad + +# NOTE: *_pose is a 3-array: 0 - x coord, 1 - y coord, 2 - orientation angle \theta + +show_animation = True + + +class eta3_path(object): + """ + eta3_path + + input + segments: list of `eta3_path_segment` instances definining a continuous path + """ + + def __init__(self, segments): + # ensure input has the correct form + assert(isinstance(segments, list) and isinstance( + segments[0], eta3_path_segment)) + # ensure that each segment begins from the previous segment's end (continuity) + for r, s in zip(segments[:-1], segments[1:]): + assert(np.array_equal(r.end_pose, s.start_pose)) + self.segments = segments + """ + eta3_path::calc_path_point + + input + normalized interpolation point along path object, 0 <= u <= len(self.segments) + returns + 2d (x,y) position vector + """ + + def calc_path_point(self, u): + assert(u >= 0 and u <= len(self.segments)) + if np.isclose(u, len(self.segments)): + segment_idx = len(self.segments) - 1 + u = 1. + else: + segment_idx = int(np.floor(u)) + u -= segment_idx + return self.segments[segment_idx].calc_point(u) + + +class eta3_path_segment(object): + """ + eta3_path_segment - constructs an eta^3 path segment based on desired shaping, eta, and curvature vector, kappa. + If either, or both, of eta and kappa are not set during initialization, they will + default to zeros. + + input + start_pose - starting pose array (x, y, \theta) + end_pose - ending pose array (x, y, \theta) + eta - shaping parameters, default=None + kappa - curvature parameters, default=None + """ + + def __init__(self, start_pose, end_pose, eta=None, kappa=None): + # make sure inputs are of the correct size + assert(len(start_pose) == 3 and len(start_pose) == len(end_pose)) + self.start_pose = start_pose + self.end_pose = end_pose + # if no eta is passed, initialize it to array of zeros + if not eta: + eta = np.zeros((6,)) + else: + # make sure that eta has correct size + assert(len(eta) == 6) + # if no kappa is passed, initialize to array of zeros + if not kappa: + kappa = np.zeros((4,)) + else: + assert(len(kappa) == 4) + # set up angle cosines and sines for simpler computations below + ca = np.cos(start_pose[2]) + sa = np.sin(start_pose[2]) + cb = np.cos(end_pose[2]) + sb = np.sin(end_pose[2]) + # 2 dimensions (x,y) x 8 coefficients per dimension + self.coeffs = np.empty((2, 8)) + # constant terms (u^0) + self.coeffs[0, 0] = start_pose[0] + self.coeffs[1, 0] = start_pose[1] + # linear (u^1) + self.coeffs[0, 1] = eta[0] * ca + self.coeffs[1, 1] = eta[0] * sa + # quadratic (u^2) + self.coeffs[0, 2] = 1. / 2 * eta[2] * \ + ca - 1. / 2 * eta[0]**2 * kappa[0] * sa + self.coeffs[1, 2] = 1. / 2 * eta[2] * \ + sa + 1. / 2 * eta[0]**2 * kappa[0] * ca + # cubic (u^3) + self.coeffs[0, 3] = 1. / 6 * eta[4] * ca - 1. / 6 * \ + (eta[0]**3 * kappa[1] + 3. * eta[0] * eta[2] * kappa[0]) * sa + self.coeffs[1, 3] = 1. / 6 * eta[4] * sa + 1. / 6 * \ + (eta[0]**3 * kappa[1] + 3. * eta[0] * eta[2] * kappa[0]) * ca + # quartic (u^4) + self.coeffs[0, 4] = 35. * (end_pose[0] - start_pose[0]) - (20. * eta[0] + 5 * eta[2] + 2. / 3 * eta[4]) * ca \ + + (5. * eta[0]**2 * kappa[0] + 2. / 3 * eta[0]**3 * kappa[1] + 2. * eta[0] * eta[2] * kappa[0]) * sa \ + - (15. * eta[1] - 5. / 2 * eta[3] + 1. / 6 * eta[5]) * cb \ + - (5. / 2 * eta[1]**2 * kappa[2] - 1. / 6 * eta[1] ** + 3 * kappa[3] - 1. / 2 * eta[1] * eta[3] * kappa[2]) * sb + self.coeffs[1, 4] = 35. * (end_pose[1] - start_pose[1]) - (20. * eta[0] + 5. * eta[2] + 2. / 3 * eta[4]) * sa \ + - (5. * eta[0]**2 * kappa[0] + 2. / 3 * eta[0]**3 * kappa[1] + 2. * eta[0] * eta[2] * kappa[0]) * ca \ + - (15. * eta[1] - 5. / 2 * eta[3] + 1. / 6 * eta[5]) * sb \ + + (5. / 2 * eta[1]**2 * kappa[2] - 1. / 6 * eta[1] ** + 3 * kappa[3] - 1. / 2 * eta[1] * eta[3] * kappa[2]) * cb + # quintic (u^5) + self.coeffs[0, 5] = -84. * (end_pose[0] - start_pose[0]) + (45. * eta[0] + 10. * eta[2] + eta[4]) * ca \ + - (10. * eta[0]**2 * kappa[0] + eta[0]**3 * kappa[1] + 3. * eta[0] * eta[2] * kappa[0]) * sa \ + + (39. * eta[1] - 7. * eta[3] + 1. / 2 * eta[5]) * cb \ + + (7. * eta[1]**2 * kappa[2] - 1. / 2 * eta[1]**3 * + kappa[3] - 3. / 2 * eta[1] * eta[3] * kappa[2]) * sb + self.coeffs[1, 5] = -84. * (end_pose[1] - start_pose[1]) + (45. * eta[0] + 10. * eta[2] + eta[4]) * sa \ + + (10. * eta[0]**2 * kappa[0] + eta[0]**3 * kappa[1] + 3. * eta[0] * eta[2] * kappa[0]) * ca \ + + (39. * eta[1] - 7. * eta[3] + 1. / 2 * eta[5]) * sb \ + - (7. * eta[1]**2 * kappa[2] - 1. / 2 * eta[1]**3 * + kappa[3] - 3. / 2 * eta[1] * eta[3] * kappa[2]) * cb + # sextic (u^6) + self.coeffs[0, 6] = 70. * (end_pose[0] - start_pose[0]) - (36. * eta[0] + 15. / 2 * eta[2] + 2. / 3 * eta[4]) * ca \ + + (15. / 2 * eta[0]**2 * kappa[0] + 2. / 3 * eta[0]**3 * kappa[1] + 2. * eta[0] * eta[2] * kappa[0]) * sa \ + - (34. * eta[1] - 13. / 2 * eta[3] + 1. / 2 * eta[5]) * cb \ + - (13. / 2 * eta[1]**2 * kappa[2] - 1. / 2 * eta[1] ** + 3 * kappa[3] - 3. / 2 * eta[1] * eta[3] * kappa[2]) * sb + self.coeffs[1, 6] = 70. * (end_pose[1] - start_pose[1]) - (36. * eta[0] + 15. / 2 * eta[2] + 2. / 3 * eta[4]) * sa \ + - (15. / 2 * eta[0]**2 * kappa[0] + 2. / 3 * eta[0]**3 * kappa[1] + 2. * eta[0] * eta[2] * kappa[0]) * ca \ + - (34. * eta[1] - 13. / 2 * eta[3] + 1. / 2 * eta[5]) * sb \ + + (13. / 2 * eta[1]**2 * kappa[2] - 1. / 2 * eta[1] ** + 3 * kappa[3] - 3. / 2 * eta[1] * eta[3] * kappa[2]) * cb + # septic (u^7) + self.coeffs[0, 7] = -20. * (end_pose[0] - start_pose[0]) + (10. * eta[0] + 2. * eta[2] + 1. / 6 * eta[4]) * ca \ + - (2. * eta[0]**2 * kappa[0] + 1. / 6 * eta[0]**3 * kappa[1] + 1. / 2 * eta[0] * eta[2] * kappa[0]) * sa \ + + (10. * eta[1] - 2. * eta[3] + 1. / 6 * eta[5]) * cb \ + + (2. * eta[1]**2 * kappa[2] - 1. / 6 * eta[1]**3 * + kappa[3] - 1. / 2 * eta[1] * eta[3] * kappa[2]) * sb + self.coeffs[1, 7] = -20. * (end_pose[1] - start_pose[1]) + (10. * eta[0] + 2. * eta[2] + 1. / 6 * eta[4]) * sa \ + + (2. * eta[0]**2 * kappa[0] + 1. / 6 * eta[0]**3 * kappa[1] + 1. / 2 * eta[0] * eta[2] * kappa[0]) * ca \ + + (10. * eta[1] - 2. * eta[3] + 1. / 6 * eta[5]) * sb \ + - (2. * eta[1]**2 * kappa[2] - 1. / 6 * eta[1]**3 * + kappa[3] - 1. / 2 * eta[1] * eta[3] * kappa[2]) * cb + + s_dot = lambda u : np.linalg.norm(self.coeffs[:, 1:].dot(np.array([1, 2.*u, 3.*u**2, 4.*u**3, 5.*u**4, 6.*u**5, 7.*u**6]))) + self.segment_length = quad(lambda u: s_dot(u), 0, 1)[0] + + """ + eta3_path_segment::calc_point + + input + u - parametric representation of a point along the segment, 0 <= u <= 1 + returns + (x,y) of point along the segment + """ + def calc_point(self, u): + assert(u >= 0 and u <= 1) + return self.coeffs.dot(np.array([1, u, u**2, u**3, u**4, u**5, u**6, u**7])) + + +def test1(): + + for i in range(10): + path_segments = [] + # segment 1: lane-change curve + start_pose = [0, 0, 0] + end_pose = [4, 3.0, 0] + # NOTE: The ordering on kappa is [kappa_A, kappad_A, kappa_B, kappad_B], with kappad_* being the curvature derivative + kappa = [0, 0, 0, 0] + eta = [i, i, 0, 0, 0, 0] + path_segments.append(eta3_path_segment( + start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa)) + + path = eta3_path(path_segments) + + # interpolate at several points along the path + ui = np.linspace(0, len(path_segments), 1001) + pos = np.empty((2, ui.size)) + for i, u in enumerate(ui): + pos[:, i] = path.calc_path_point(u) + + if show_animation: + # plot the path + plt.plot(pos[0, :], pos[1, :]) + plt.pause(1.0) + + if show_animation: + plt.close("all") + + +def test2(): + + for i in range(10): + path_segments = [] + # segment 1: lane-change curve + start_pose = [0, 0, 0] + end_pose = [4, 3.0, 0] + # NOTE: The ordering on kappa is [kappa_A, kappad_A, kappa_B, kappad_B], with kappad_* being the curvature derivative + kappa = [0, 0, 0, 0] + eta = [0, 0, (i - 5) * 20, (5 - i) * 20, 0, 0] + path_segments.append(eta3_path_segment( + start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa)) + + path = eta3_path(path_segments) + + # interpolate at several points along the path + ui = np.linspace(0, len(path_segments), 1001) + pos = np.empty((2, ui.size)) + for i, u in enumerate(ui): + pos[:, i] = path.calc_path_point(u) + + if show_animation: + # plot the path + plt.plot(pos[0, :], pos[1, :]) + plt.pause(1.0) + + if show_animation: + plt.close("all") + + +def test3(): + path_segments = [] + + # segment 1: lane-change curve + start_pose = [0, 0, 0] + end_pose = [4, 1.5, 0] + # NOTE: The ordering on kappa is [kappa_A, kappad_A, kappa_B, kappad_B], with kappad_* being the curvature derivative + kappa = [0, 0, 0, 0] + eta = [4.27, 4.27, 0, 0, 0, 0] + path_segments.append(eta3_path_segment( + start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa)) + + # segment 2: line segment + start_pose = [4, 1.5, 0] + end_pose = [5.5, 1.5, 0] + kappa = [0, 0, 0, 0] + eta = [0, 0, 0, 0, 0, 0] + path_segments.append(eta3_path_segment( + start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa)) + + # segment 3: cubic spiral + start_pose = [5.5, 1.5, 0] + end_pose = [7.4377, 1.8235, 0.6667] + kappa = [0, 0, 1, 1] + eta = [1.88, 1.88, 0, 0, 0, 0] + path_segments.append(eta3_path_segment( + start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa)) + + # segment 4: generic twirl arc + start_pose = [7.4377, 1.8235, 0.6667] + end_pose = [7.8, 4.3, 1.8] + kappa = [1, 1, 0.5, 0] + eta = [7, 10, 10, -10, 4, 4] + path_segments.append(eta3_path_segment( + start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa)) + + # segment 5: circular arc + start_pose = [7.8, 4.3, 1.8] + end_pose = [5.4581, 5.8064, 3.3416] + kappa = [0.5, 0, 0.5, 0] + eta = [2.98, 2.98, 0, 0, 0, 0] + path_segments.append(eta3_path_segment( + start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa)) + + # construct the whole path + path = eta3_path(path_segments) + + # interpolate at several points along the path + ui = np.linspace(0, len(path_segments), 1001) + pos = np.empty((2, ui.size)) + for i, u in enumerate(ui): + pos[:, i] = path.calc_path_point(u) + + # plot the path + + if show_animation: + plt.figure('Path from Reference') + plt.plot(pos[0, :], pos[1, :]) + plt.xlabel('x') + plt.ylabel('y') + plt.title('Path') + plt.pause(1.0) + + plt.show() + + +def main(): + """ + recreate path from reference (see Table 1) + """ + test1() + test2() + test3() + + +if __name__ == '__main__': + main() diff --git a/PathPlanning/FrenetOptimalTrajectory/frenet_optimal_trajectory.py b/PathPlanning/FrenetOptimalTrajectory/frenet_optimal_trajectory.py index 3957df90..9d6c62e5 100644 --- a/PathPlanning/FrenetOptimalTrajectory/frenet_optimal_trajectory.py +++ b/PathPlanning/FrenetOptimalTrajectory/frenet_optimal_trajectory.py @@ -42,11 +42,11 @@ KLON = 1.0 show_animation = True -class quinic_polynomial: +class quintic_polynomial: def __init__(self, xs, vxs, axs, xe, vxe, axe, T): - # calc coefficient of quinic polynomial + # calc coefficient of quintic polynomial self.xs = xs self.vxs = vxs self.axs = axs @@ -87,12 +87,17 @@ class quinic_polynomial: return xt + def calc_third_derivative(self, t): + xt = 6 * self.a3 + 24 * self.a4 * t + 60 * self.a5 * t**2 + + return xt + class quartic_polynomial: def __init__(self, xs, vxs, axs, vxe, axe, T): - # calc coefficient of quinic polynomial + # calc coefficient of quintic polynomial self.xs = xs self.vxs = vxs self.axs = axs @@ -129,6 +134,11 @@ class quartic_polynomial: return xt + def calc_third_derivative(self, t): + xt = 6 * self.a3 + 24 * self.a4 * t + + return xt + class Frenet_path: @@ -137,9 +147,11 @@ class Frenet_path: self.d = [] self.d_d = [] self.d_dd = [] + self.d_ddd = [] self.s = [] self.s_d = [] self.s_dd = [] + self.s_ddd = [] self.cd = 0.0 self.cv = 0.0 self.cf = 0.0 @@ -155,17 +167,22 @@ def calc_frenet_paths(c_speed, c_d, c_d_d, c_d_dd, s0): frenet_paths = [] + # generate path to each offset goal for di in np.arange(-MAX_ROAD_WIDTH, MAX_ROAD_WIDTH, D_ROAD_W): + + # Lateral motion planning for Ti in np.arange(MINT, MAXT, DT): fp = Frenet_path() - lat_qp = quinic_polynomial(c_d, c_d_d, c_d_dd, di, 0.0, 0.0, Ti) + lat_qp = quintic_polynomial(c_d, c_d_d, c_d_dd, di, 0.0, 0.0, Ti) fp.t = [t for t in np.arange(0.0, Ti, DT)] fp.d = [lat_qp.calc_point(t) for t in fp.t] fp.d_d = [lat_qp.calc_first_derivative(t) for t in fp.t] fp.d_dd = [lat_qp.calc_second_derivative(t) for t in fp.t] + fp.d_ddd = [lat_qp.calc_third_derivative(t) for t in fp.t] + # Loongitudinal motion planning (Velocity keeping) for tv in np.arange(TARGET_SPEED - D_T_S * N_S_SAMPLE, TARGET_SPEED + D_T_S * N_S_SAMPLE, D_T_S): tfp = copy.deepcopy(fp) lon_qp = quartic_polynomial(s0, c_speed, 0.0, tv, 0.0, Ti) @@ -173,10 +190,16 @@ def calc_frenet_paths(c_speed, c_d, c_d_d, c_d_dd, s0): tfp.s = [lon_qp.calc_point(t) for t in fp.t] tfp.s_d = [lon_qp.calc_first_derivative(t) for t in fp.t] tfp.s_dd = [lon_qp.calc_second_derivative(t) for t in fp.t] + tfp.s_ddd = [lon_qp.calc_third_derivative(t) for t in fp.t] - tfp.cd = KJ * sum(tfp.d_dd) + KT * Ti + KD * tfp.d[-1]**2 - tfp.cv = KJ * sum(tfp.s_dd) + KT * Ti + KD * \ - (TARGET_SPEED - tfp.s_d[-1])**2 + Jp = sum(np.power(tfp.d_ddd, 2)) # square of jerk + Js = sum(np.power(tfp.s_ddd, 2)) # square of jerk + + # square of diff from target speed + ds = (TARGET_SPEED - tfp.s_d[-1])**2 + + tfp.cd = KJ * Jp + KT * Ti + KD * tfp.d[-1]**2 + tfp.cv = KJ * Js + KT * Ti + KD * ds tfp.cf = KLAT * tfp.cd + KLON * tfp.cv frenet_paths.append(tfp) diff --git a/PathPlanning/HybridAStar/hybrid_a_star.py b/PathPlanning/HybridAStar/hybrid_a_star.py new file mode 100644 index 00000000..4e1ee11d --- /dev/null +++ b/PathPlanning/HybridAStar/hybrid_a_star.py @@ -0,0 +1,222 @@ +""" + +Hybrid A* path planning + +author: Atsushi Sakai (@Atsushi_twi) + +""" + +import sys +sys.path.append("../ReedsSheppPath/") + +import math +import numpy as np +import scipy.spatial +import matplotlib.pyplot as plt +import reeds_shepp_path_planning as rs +import heapq + +EXTEND_AREA = 5.0 # [m] +H_COST = 1.0 + +show_animation = True + + +class Node: + + def __init__(self, xind, yind, yawind, direction, x, y, yaw, directions, steer, cost, pind): + # store kd-tree + self.xind = xind + self.yind = yind + self.yawind = yawind + self.direction = direction + self.xlist = x + self.ylist = y + self.yawlist = yaw + self.directionlist = directions + self.steer = steer + self.cost = cost + self.pind = pind + + +class KDTree: + """ + Nearest neighbor search class with KDTree + """ + + def __init__(self, data): + # store kd-tree + self.tree = scipy.spatial.cKDTree(data) + + def search(self, inp, k=1): + """ + Search NN + inp: input data, single frame or multi frame + """ + + if len(inp.shape) >= 2: # multi input + index = [] + dist = [] + + for i in inp.T: + idist, iindex = self.tree.query(i, k=k) + index.append(iindex) + dist.append(idist) + + return index, dist + else: + dist, index = self.tree.query(inp, k=k) + return index, dist + + def search_in_distance(self, inp, r): + """ + find points with in a distance r + """ + + index = self.tree.query_ball_point(inp, r) + return index + + +class Config: + + def __init__(self, ox, oy, xyreso, yawreso): + min_x_m = min(ox) - EXTEND_AREA + min_y_m = min(oy) - EXTEND_AREA + max_x_m = max(ox) + EXTEND_AREA + max_y_m = max(oy) + EXTEND_AREA + + ox.append(min_x_m) + oy.append(min_y_m) + ox.append(max_x_m) + oy.append(max_y_m) + + self.minx = int(min_x_m / xyreso) + self.miny = int(min_y_m / xyreso) + self.maxx = int(max_x_m / xyreso) + self.maxy = int(max_y_m / xyreso) + + self.xw = int(self.maxx - self.minx) + self.yw = int(self.maxy - self.miny) + + self.minyaw = int(- math.pi / yawreso) - 1 + self.maxyaw = int(math.pi / yawreso) + self.yaww = int(self.maxyaw - self.minyaw) + + +def analytic_expantion(current, ngoal, c, ox, oy, kdtree): + + return False, None # no update + + +def hybrid_a_star_planning(start, goal, ox, oy, xyreso, yawreso): + """ + start + goal + ox: x position list of Obstacles [m] + oy: y position list of Obstacles [m] + xyreso: grid resolution [m] + yawreso: yaw angle resolution [rad] + """ + + start[2], goal[2] = rs.pi_2_pi(start[2]), rs.pi_2_pi(goal[2]) + tox, toy = ox[:], oy[:] + + obkdtree = KDTree(np.vstack((tox, toy)).T) + + c = Config(tox, toy, xyreso, yawreso) + + nstart = Node(int(start[0] / xyreso), int(start[1] / xyreso), int(start[2] / yawreso), + True, [start[0]], [start[1]], [start[2]], [True], 0.0, 0.0, -1) + ngoal = Node(int(goal[0] / xyreso), int(goal[1] / xyreso), int(goal[2] / yawreso), + True, [goal[0]], [goal[1]], [goal[2]], [True], 0.0, 0.0, -1) + + openList, closedList = {}, {} + h = [] + # goalqueue = queue.PriorityQueue() + pq = [] + openList[calc_index(nstart, c)] = nstart + heapq.heappush(pq, (calc_index(nstart, c), calc_cost(nstart, h, ngoal, c))) + + while True: + if not openList: + print("Error: Cannot find path, No open set") + return [], [], [] + + c_id, cost = heapq.heappop(pq) + current = openList.pop(c_id) + closedList[c_id] = current + + isupdated, fpath = analytic_expantion( + current, ngoal, c, ox, oy, obkdtree) + + # print(current) + + rx, ry, ryaw = [], [], [] + + return rx, ry, ryaw + + +def calc_cost(n, h, ngoal, c): + + hcost = 1.0 + + return (n.cost + H_COST * hcost) + + +def calc_index(node, c): + ind = (node.yawind - c.minyaw) * c.xw * c.yw + \ + (node.yind - c.miny) * c.xw + (node.xind - c.minx) + + if ind <= 0: + print("Error(calc_index):", ind) + + return ind + + +def main(): + print("Start Hybrid A* planning") + + ox, oy = [], [] + + for i in range(60): + ox.append(i) + oy.append(0.0) + for i in range(60): + ox.append(60.0) + oy.append(i) + for i in range(61): + ox.append(i) + oy.append(60.0) + for i in range(61): + ox.append(0.0) + oy.append(i) + for i in range(40): + ox.append(20.0) + oy.append(i) + for i in range(40): + ox.append(40.0) + oy.append(60.0 - i) + + # Set Initial parameters + start = [10.0, 10.0, math.radians(90.0)] + goal = [50.0, 50.0, math.radians(-90.0)] + + xyreso = 2.0 + yawreso = math.radians(15.0) + + rx, ry, ryaw = hybrid_a_star_planning( + start, goal, ox, oy, xyreso, yawreso) + + plt.plot(ox, oy, ".k") + rs.plot_arrow(start[0], start[1], start[2]) + rs.plot_arrow(goal[0], goal[1], goal[2]) + + plt.grid(True) + plt.axis("equal") + plt.show() + + print(__file__ + " start!!") + + +if __name__ == '__main__': + main() diff --git a/PathPlanning/InformedRRTStar/animation.gif b/PathPlanning/InformedRRTStar/animation.gif new file mode 100644 index 00000000..9bcc8c42 Binary files /dev/null and b/PathPlanning/InformedRRTStar/animation.gif differ diff --git a/PathPlanning/InformedRRTStar/informed_rrt_star.py b/PathPlanning/InformedRRTStar/informed_rrt_star.py new file mode 100644 index 00000000..5b4a28ee --- /dev/null +++ b/PathPlanning/InformedRRTStar/informed_rrt_star.py @@ -0,0 +1,335 @@ +""" +Informed RRT* path planning + +author: Karan Chawla + Atsushi Sakai(@Atsushi_twi) + +Reference: Informed RRT*: Optimal Sampling-based Path Planning Focused via +Direct Sampling of an Admissible Ellipsoidal Heuristichttps://arxiv.org/pdf/1404.2334.pdf + +""" + + +import random +import numpy as np +import math +import copy +import matplotlib.pyplot as plt + +show_animation = True + + +class InformedRRTStar(): + + def __init__(self, start, goal, + obstacleList, randArea, + expandDis=0.5, goalSampleRate=10, maxIter=200): + + self.start = Node(start[0], start[1]) + self.goal = Node(goal[0], goal[1]) + self.minrand = randArea[0] + self.maxrand = randArea[1] + self.expandDis = expandDis + self.goalSampleRate = goalSampleRate + self.maxIter = maxIter + self.obstacleList = obstacleList + + def InformedRRTStarSearch(self, animation=True): + + self.nodeList = [self.start] + # max length we expect to find in our 'informed' sample space, starts as infinite + cBest = float('inf') + pathLen = float('inf') + solutionSet = set() + path = None + + # Computing the sampling space + cMin = math.sqrt(pow(self.start.x - self.goal.x, 2) + + pow(self.start.y - self.goal.y, 2)) + xCenter = np.matrix([[(self.start.x + self.goal.x) / 2.0], + [(self.start.y + self.goal.y) / 2.0], [0]]) + a1 = np.matrix([[(self.goal.x - self.start.x) / cMin], + [(self.goal.y - self.start.y) / cMin], [0]]) + etheta = math.atan2(a1[1], a1[0]) + # first column of idenity matrix transposed + id1_t = np.matrix([1.0, 0.0, 0.0]) + M = np.dot(a1, id1_t) + U, S, Vh = np.linalg.svd(M, 1, 1) + C = np.dot(np.dot(U, np.diag( + [1.0, 1.0, np.linalg.det(U) * np.linalg.det(np.transpose(Vh))])), Vh) + + for i in range(self.maxIter): + # Sample space is defined by cBest + # cMin is the minimum distance between the start point and the goal + # xCenter is the midpoint between the start and the goal + # cBest changes when a new path is found + + rnd = self.informed_sample(cBest, cMin, xCenter, C) + nind = self.getNearestListIndex(self.nodeList, rnd) + nearestNode = self.nodeList[nind] + # steer + theta = math.atan2(rnd[1] - nearestNode.y, rnd[0] - nearestNode.x) + newNode = self.getNewNode(theta, nind, nearestNode) + d = self.lineCost(nearestNode, newNode) + + isCollision = self.__CollisionCheck(newNode, self.obstacleList) + isCollisionEx = self.check_collision_extend(nearestNode, theta, d) + + if isCollision and isCollisionEx: + nearInds = self.findNearNodes(newNode) + newNode = self.chooseParent(newNode, nearInds) + + self.nodeList.append(newNode) + self.rewire(newNode, nearInds) + + if self.isNearGoal(newNode): + solutionSet.add(newNode) + lastIndex = len(self.nodeList) - 1 + tempPath = self.getFinalCourse(lastIndex) + tempPathLen = self.getPathLen(tempPath) + if tempPathLen < pathLen: + path = tempPath + cBest = tempPathLen + + if animation: + self.drawGraph(xCenter=xCenter, + cBest=cBest, cMin=cMin, + etheta=etheta, rnd=rnd) + + return path + + def chooseParent(self, newNode, nearInds): + if len(nearInds) == 0: + return newNode + + dList = [] + for i in nearInds: + dx = newNode.x - self.nodeList[i].x + dy = newNode.y - self.nodeList[i].y + d = math.sqrt(dx ** 2 + dy ** 2) + theta = math.atan2(dy, dx) + if self.check_collision_extend(self.nodeList[i], theta, d): + dList.append(self.nodeList[i].cost + d) + else: + dList.append(float('inf')) + + minCost = min(dList) + minInd = nearInds[dList.index(minCost)] + + if minCost == float('inf'): + print("mincost is inf") + return newNode + + newNode.cost = minCost + newNode.parent = minInd + + return newNode + + def findNearNodes(self, newNode): + nnode = len(self.nodeList) + r = 50.0 * math.sqrt((math.log(nnode) / nnode)) + dlist = [(node.x - newNode.x) ** 2 + + (node.y - newNode.y) ** 2 for node in self.nodeList] + nearinds = [dlist.index(i) for i in dlist if i <= r ** 2] + return nearinds + + def informed_sample(self, cMax, cMin, xCenter, C): + if cMax < float('inf'): + r = [cMax / 2.0, + math.sqrt(cMax**2 - cMin**2) / 2.0, + math.sqrt(cMax**2 - cMin**2) / 2.0] + L = np.diag(r) + xBall = self.sampleUnitBall() + rnd = np.dot(np.dot(C, L), xBall) + xCenter + rnd = [rnd[(0, 0)], rnd[(1, 0)]] + else: + rnd = self.sampleFreeSpace() + + return rnd + + def sampleUnitBall(self): + a = random.random() + b = random.random() + + if b < a: + a, b = b, a + + sample = (b * math.cos(2 * math.pi * a / b), + b * math.sin(2 * math.pi * a / b)) + return np.array([[sample[0]], [sample[1]], [0]]) + + def sampleFreeSpace(self): + if random.randint(0, 100) > self.goalSampleRate: + rnd = [random.uniform(self.minrand, self.maxrand), + random.uniform(self.minrand, self.maxrand)] + else: + rnd = [self.goal.x, self.goal.y] + + return rnd + + def getPathLen(self, path): + pathLen = 0 + for i in range(1, len(path)): + node1_x = path[i][0] + node1_y = path[i][1] + node2_x = path[i - 1][0] + node2_y = path[i - 1][1] + pathLen += math.sqrt((node1_x - node2_x) ** + 2 + (node1_y - node2_y)**2) + + return pathLen + + def lineCost(self, node1, node2): + return math.sqrt((node1.x - node2.x)**2 + (node1.y - node2.y)**2) + + def getNearestListIndex(self, nodes, rnd): + dList = [(node.x - rnd[0])**2 + + (node.y - rnd[1])**2 for node in nodes] + minIndex = dList.index(min(dList)) + return minIndex + + def __CollisionCheck(self, newNode, obstacleList): + for (ox, oy, size) in obstacleList: + dx = ox - newNode.x + dy = oy - newNode.y + d = dx * dx + dy * dy + if d <= 1.1 * size**2: + return False # collision + + return True # safe + + def getNewNode(self, theta, nind, nearestNode): + newNode = copy.deepcopy(nearestNode) + + newNode.x += self.expandDis * math.cos(theta) + newNode.y += self.expandDis * math.sin(theta) + + newNode.cost += self.expandDis + newNode.parent = nind + return newNode + + def isNearGoal(self, node): + d = self.lineCost(node, self.goal) + if d < self.expandDis: + return True + return False + + def rewire(self, newNode, nearInds): + nnode = len(self.nodeList) + for i in nearInds: + nearNode = self.nodeList[i] + + d = math.sqrt((nearNode.x - newNode.x)**2 + + (nearNode.y - newNode.y)**2) + + scost = newNode.cost + d + + if nearNode.cost > scost: + theta = math.atan2(newNode.y - nearNode.y, + newNode.x - nearNode.x) + if self.check_collision_extend(nearNode, theta, d): + nearNode.parent = nnode - 1 + nearNode.cost = scost + + def check_collision_extend(self, nearNode, theta, d): + tmpNode = copy.deepcopy(nearNode) + + for i in range(int(d / self.expandDis)): + tmpNode.x += self.expandDis * math.cos(theta) + tmpNode.y += self.expandDis * math.sin(theta) + if not self.__CollisionCheck(tmpNode, self.obstacleList): + return False + + return True + + def getFinalCourse(self, lastIndex): + path = [[self.goal.x, self.goal.y]] + while self.nodeList[lastIndex].parent is not None: + node = self.nodeList[lastIndex] + path.append([node.x, node.y]) + lastIndex = node.parent + path.append([self.start.x, self.start.y]) + return path + + def drawGraph(self, xCenter=None, cBest=None, cMin=None, etheta=None, rnd=None): + + plt.clf() + if rnd is not None: + plt.plot(rnd[0], rnd[1], "^k") + if cBest != float('inf'): + self.plot_ellipse(xCenter, cBest, cMin, etheta) + + for node in self.nodeList: + if node.parent is not None: + if node.x or node.y is not None: + plt.plot([node.x, self.nodeList[node.parent].x], [ + node.y, self.nodeList[node.parent].y], "-g") + + for (ox, oy, size) in self.obstacleList: + plt.plot(ox, oy, "ok", ms=30 * size) + + plt.plot(self.start.x, self.start.y, "xr") + plt.plot(self.goal.x, self.goal.y, "xr") + plt.axis([-2, 15, -2, 15]) + plt.grid(True) + plt.pause(0.01) + + def plot_ellipse(self, xCenter, cBest, cMin, etheta): + + a = math.sqrt(cBest**2 - cMin**2) / 2.0 + b = cBest / 2.0 + angle = math.pi / 2.0 - etheta + cx = xCenter[0] + cy = xCenter[1] + + t = np.arange(0, 2 * math.pi + 0.1, 0.1) + x = [a * math.cos(it) for it in t] + y = [b * math.sin(it) for it in t] + R = np.matrix([[math.cos(angle), math.sin(angle)], + [-math.sin(angle), math.cos(angle)]]) + fx = R * np.matrix([x, y]) + px = np.array(fx[0, :] + cx).flatten() + py = np.array(fx[1, :] + cy).flatten() + plt.plot(cx, cy, "xc") + plt.plot(px, py, "--c") + + +class Node(): + + def __init__(self, x, y): + self.x = x + self.y = y + self.cost = 0.0 + self.parent = None + + +def main(): + print("Start informed rrt star planning") + + # create obstacles + obstacleList = [ + (5, 5, 0.5), + (9, 6, 1), + (7, 5, 1), + (1, 5, 1), + (3, 6, 1), + (7, 9, 1) + ] + + # Set params + rrt = InformedRRTStar(start=[0, 0], goal=[5, 10], + randArea=[-2, 15], obstacleList=obstacleList) + path = rrt.InformedRRTStarSearch(animation=show_animation) + print("Done!!") + + # Plot path + if show_animation: + rrt.drawGraph() + plt.plot([x for (x, y) in path], [y for (x, y) in path], '-r') + plt.grid(True) + plt.pause(0.01) + plt.show() + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/PathPlanning/LQRPlanner/LQRplanner.py b/PathPlanning/LQRPlanner/LQRplanner.py new file mode 100644 index 00000000..210f2a02 --- /dev/null +++ b/PathPlanning/LQRPlanner/LQRplanner.py @@ -0,0 +1,160 @@ +""" + +LQR local path planning + +author: Atsushi Sakai (@Atsushi_twi) + +""" + +import matplotlib.pyplot as plt +import numpy as np +import scipy.linalg as la +import math +import random + +show_animation = True + +MAX_TIME = 100.0 # Maximum simulation time +DT = 0.1 # Time tick + + +def LQRplanning(sx, sy, gx, gy): + + rx, ry = [sx], [sy] + + x = np.matrix([sx - gx, sy - gy]).T # State vector + + # Linear system model + A, B = get_system_model() + + found_path = False + + time = 0.0 + while time <= MAX_TIME: + time += DT + + u = LQR_control(A, B, x) + + x = A * x + B * u + + rx.append(x[0, 0] + gx) + ry.append(x[1, 0] + gy) + + d = math.sqrt((gx - rx[-1])**2 + (gy - ry[-1])**2) + if d <= 0.1: + # print("Goal!!") + found_path = True + break + + # animation + if show_animation: + plt.plot(sx, sy, "or") + plt.plot(gx, gy, "ob") + plt.plot(rx, ry, "-r") + plt.axis("equal") + plt.pause(1.0) + + if not found_path: + print("Cannot found path") + return [], [] + + return rx, ry + + +def solve_DARE(A, B, Q, R): + """ + solve a discrete time_Algebraic Riccati equation (DARE) + """ + X = Q + maxiter = 150 + eps = 0.01 + + for i in range(maxiter): + Xn = A.T * X * A - A.T * X * B * \ + la.inv(R + B.T * X * B) * B.T * X * A + Q + if (abs(Xn - X)).max() < eps: + X = Xn + break + X = Xn + + return Xn + + +def dlqr(A, B, Q, R): + """Solve the discrete time lqr controller. + x[k+1] = A x[k] + B u[k] + cost = sum x[k].T*Q*x[k] + u[k].T*R*u[k] + # ref Bertsekas, p.151 + """ + + # first, try to solve the ricatti equation + X = solve_DARE(A, B, Q, R) + + # compute the LQR gain + K = np.matrix(la.inv(B.T * X * B + R) * (B.T * X * A)) + + eigVals, eigVecs = la.eig(A - B * K) + + return K, X, eigVals + + +def get_system_model(): + + A = np.matrix([[DT, 1.0], + [0.0, DT]]) + B = np.matrix([0.0, 1.0]).T + + return A, B + + +def LQR_control(A, B, x): + + Kopt, X, ev = dlqr(A, B, np.eye(2), np.eye(1)) + + u = -Kopt * x + + return u + + +def main(): + print(__file__ + " start!!") + + ntest = 10 # number of goal + area = 100.0 # sampling area + + for i in range(ntest): + sx = 6.0 + sy = 6.0 + gx = random.uniform(-area, area) + gy = random.uniform(-area, area) + + rx, ry = LQRplanning(sx, sy, gx, gy) + + if show_animation: + plt.plot(sx, sy, "or") + plt.plot(gx, gy, "ob") + plt.plot(rx, ry, "-r") + plt.axis("equal") + plt.pause(1.0) + + +def main1(): + print(__file__ + " start!!") + + sx = 6.0 + sy = 6.0 + gx = 10.0 + gy = 10.0 + + rx, ry = LQRplanning(sx, sy, gx, gy) + + if show_animation: + plt.plot(sx, sy, "or") + plt.plot(gx, gy, "ob") + plt.plot(rx, ry, "-r") + plt.axis("equal") + plt.show() + + +if __name__ == '__main__': + main() diff --git a/PathPlanning/LQRPlanner/animation.gif b/PathPlanning/LQRPlanner/animation.gif new file mode 100644 index 00000000..694e76c7 Binary files /dev/null and b/PathPlanning/LQRPlanner/animation.gif differ diff --git a/PathPlanning/LQRRRTStar/animation.gif b/PathPlanning/LQRRRTStar/animation.gif new file mode 100644 index 00000000..eaa6d208 Binary files /dev/null and b/PathPlanning/LQRRRTStar/animation.gif differ diff --git a/PathPlanning/LQRRRTStar/lqr_rrt_star.py b/PathPlanning/LQRRRTStar/lqr_rrt_star.py new file mode 100644 index 00000000..326c787f --- /dev/null +++ b/PathPlanning/LQRRRTStar/lqr_rrt_star.py @@ -0,0 +1,314 @@ +""" + +Path planning code with LQR RRT* + +author: AtsushiSakai(@Atsushi_twi) + +""" + +import sys +sys.path.append("../LQRPlanner/") + +import random +import math +import copy +import numpy as np +import matplotlib.pyplot as plt +import LQRplanner + +show_animation = True + +LQRplanner.show_animation = False + +STEP_SIZE = 0.05 # step size of local path +XYTH = 0.5 # [m] acceptance xy distance in final paths + + +class RRT(): + """ + Class for RRT Planning + """ + + def __init__(self, start, goal, obstacleList, randArea, + goalSampleRate=10, maxIter=200): + """ + Setting Parameter + + start:Start Position [x,y] + goal:Goal Position [x,y] + obstacleList:obstacle Positions [[x,y,size],...] + randArea:Ramdom Samping Area [min,max] + + """ + self.start = Node(start[0], start[1]) + self.end = Node(goal[0], goal[1]) + self.minrand = randArea[0] + self.maxrand = randArea[1] + self.goalSampleRate = goalSampleRate + self.maxIter = maxIter + self.obstacleList = obstacleList + + def planning(self, animation=True): + """ + Pathplanning + + animation: flag for animation on or off + """ + + self.nodeList = [self.start] + for i in range(self.maxIter): + rnd = self.get_random_point() + nind = self.get_nearest_index(self.nodeList, rnd) + + newNode = self.steer(rnd, nind) + if newNode is None: + continue + + if self.check_collision(newNode, self.obstacleList): + nearinds = self.find_near_nodes(newNode) + newNode = self.choose_parent(newNode, nearinds) + if newNode is None: + continue + self.nodeList.append(newNode) + self.rewire(newNode, nearinds) + + if animation and i % 5 == 0: + self.draw_graph(rnd=rnd) + + # generate coruse + lastIndex = self.get_best_last_index() + if lastIndex is None: + return None + path = self.gen_final_course(lastIndex) + return path + + def choose_parent(self, newNode, nearinds): + if len(nearinds) == 0: + return newNode + + dlist = [] + for i in nearinds: + tNode = self.steer(newNode, i) + if tNode is None: + continue + + if self.check_collision(tNode, self.obstacleList): + dlist.append(tNode.cost) + else: + dlist.append(float("inf")) + + mincost = min(dlist) + minind = nearinds[dlist.index(mincost)] + + if mincost == float("inf"): + print("mincost is inf") + return newNode + + newNode = self.steer(newNode, minind) + + return newNode + + def pi_2_pi(self, angle): + return (angle + math.pi) % (2*math.pi) - math.pi + + def sample_path(self, wx, wy, step): + + px, py, clen = [], [], [] + + for i in range(len(wx) - 1): + + for t in np.arange(0.0, 1.0, step): + px.append(t * wx[i + 1] + (1.0 - t) * wx[i]) + py.append(t * wy[i + 1] + (1.0 - t) * wy[i]) + + dx = np.diff(px) + dy = np.diff(py) + + clen = [math.sqrt(idx**2 + idy**2) for (idx, idy) in zip(dx, dy)] + + return px, py, clen + + def steer(self, rnd, nind): + + nearestNode = self.nodeList[nind] + + wx, wy = LQRplanner.LQRplanning( + nearestNode.x, nearestNode.y, rnd.x, rnd.y) + + px, py, clen = self.sample_path(wx, wy, STEP_SIZE) + + if px is None: + return None + + newNode = copy.deepcopy(nearestNode) + newNode.x = px[-1] + newNode.y = py[-1] + + newNode.path_x = px + newNode.path_y = py + newNode.cost += sum([abs(c) for c in clen]) + newNode.parent = nind + + return newNode + + def get_random_point(self): + + if random.randint(0, 100) > self.goalSampleRate: + rnd = [random.uniform(self.minrand, self.maxrand), + random.uniform(self.minrand, self.maxrand), + random.uniform(-math.pi, math.pi) + ] + else: # goal point sampling + rnd = [self.end.x, self.end.y] + + node = Node(rnd[0], rnd[1]) + + return node + + def get_best_last_index(self): + # print("get_best_last_index") + + goalinds = [] + for (i, node) in enumerate(self.nodeList): + if self.calc_dist_to_goal(node.x, node.y) <= XYTH: + goalinds.append(i) + + if len(goalinds) == 0: + return None + + mincost = min([self.nodeList[i].cost for i in goalinds]) + for i in goalinds: + if self.nodeList[i].cost == mincost: + return i + + return None + + def gen_final_course(self, goalind): + path = [[self.end.x, self.end.y]] + while self.nodeList[goalind].parent is not None: + node = self.nodeList[goalind] + for (ix, iy) in zip(reversed(node.path_x), reversed(node.path_y)): + path.append([ix, iy]) + goalind = node.parent + path.append([self.start.x, self.start.y]) + return path + + def calc_dist_to_goal(self, x, y): + return np.linalg.norm([x - self.end.x, y - self.end.y]) + + def find_near_nodes(self, newNode): + nnode = len(self.nodeList) + r = 50.0 * math.sqrt((math.log(nnode) / nnode)) + dlist = [(node.x - newNode.x) ** 2 + + (node.y - newNode.y) ** 2 + for node in self.nodeList] + nearinds = [dlist.index(i) for i in dlist if i <= r ** 2] + return nearinds + + def rewire(self, newNode, nearinds): + + nnode = len(self.nodeList) + + for i in nearinds: + nearNode = self.nodeList[i] + tNode = self.steer(nearNode, nnode - 1) + if tNode is None: + continue + + obstacleOK = self.check_collision(tNode, self.obstacleList) + imporveCost = nearNode.cost > tNode.cost + + if obstacleOK and imporveCost: + # print("rewire") + self.nodeList[i] = tNode + + def draw_graph(self, rnd=None): + plt.clf() + if rnd is not None: + plt.plot(rnd.x, rnd.y, "^k") + + for node in self.nodeList: + if node.parent is not None: + plt.plot(node.path_x, node.path_y, "-g") + + for (ox, oy, size) in self.obstacleList: + plt.plot(ox, oy, "ok", ms=30 * size) + + plt.plot(self.start.x, self.start.y, "or") + plt.plot(self.end.x, self.end.y, "or") + + plt.axis([-2, 15, -2, 15]) + plt.grid(True) + plt.pause(0.01) + + def get_nearest_index(self, nodeList, rnd): + dlist = [(node.x - rnd.x) ** 2 + + (node.y - rnd.y) ** 2 + for node in nodeList] + minind = dlist.index(min(dlist)) + + return minind + + def check_collision(self, node, obstacleList): + + px = np.array(node.path_x) + py = np.array(node.path_y) + + for (ox, oy, size) in obstacleList: + dx = ox - px + dy = oy - py + d = dx ** 2 + dy ** 2 + dmin = min(d) + if dmin <= size ** 2: + return False # collision + + return True # safe + + +class Node(): + """ + RRT Node + """ + + def __init__(self, x, y): + self.x = x + self.y = y + self.path_x = [] + self.path_y = [] + self.cost = 0.0 + self.parent = None + + +def main(): + print("Start rrt start planning") + + # ====Search Path with RRT==== + obstacleList = [ + (5, 5, 1), + (4, 6, 1), + (4, 7.5, 1), + (4, 9, 1), + (6, 5, 1), + (7, 5, 1) + ] # [x,y,size] + + # Set Initial parameters + start = [0.0, 0.0] + goal = [6.0, 7.0] + + rrt = RRT(start, goal, randArea=[-2.0, 15.0], obstacleList=obstacleList) + path = rrt.planning(animation=show_animation) + + # Draw final path + if show_animation: + rrt.draw_graph() + plt.plot([x for (x, y) in path], [y for (x, y) in path], '-r') + plt.grid(True) + plt.pause(0.001) + plt.show() + + print("Done") + + +if __name__ == '__main__': + main() diff --git a/PathPlanning/ModelPredictiveTrajectoryGenerator/motion_model.py b/PathPlanning/ModelPredictiveTrajectoryGenerator/motion_model.py index 6319c5a0..39675198 100644 --- a/PathPlanning/ModelPredictiveTrajectoryGenerator/motion_model.py +++ b/PathPlanning/ModelPredictiveTrajectoryGenerator/motion_model.py @@ -18,13 +18,7 @@ class State: def pi_2_pi(angle): - while(angle > math.pi): - angle = angle - 2.0 * math.pi - - while(angle < -math.pi): - angle = angle + 2.0 * math.pi - - return angle + return (angle + math.pi) % (2*math.pi) - math.pi def update(state, v, delta, dt, L): diff --git a/PathPlanning/QuinticPolynomialsPlanner/quintic_polynomials_planner.py b/PathPlanning/QuinticPolynomialsPlanner/quintic_polynomials_planner.py index 938f19ba..99d7fc7d 100644 --- a/PathPlanning/QuinticPolynomialsPlanner/quintic_polynomials_planner.py +++ b/PathPlanning/QuinticPolynomialsPlanner/quintic_polynomials_planner.py @@ -15,8 +15,8 @@ import matplotlib.pyplot as plt import math # parameter -MAX_T = 100.0 -MIN_T = 5.0 +MAX_T = 100.0 # maximum time to the goal [s] +MIN_T = 5.0 # minimum time to the goal[s] show_animation = True @@ -66,8 +66,13 @@ class quinic_polynomial: return xt + def calc_third_derivative(self, t): + xt = 6 * self.a3 + 24 * self.a4 * t + 60 * self.a5 * t**2 -def quinic_polynomials_planner(sx, sy, syaw, sv, sa, gx, gy, gyaw, gv, ga, max_accel, dt): + return xt + + +def quinic_polynomials_planner(sx, sy, syaw, sv, sa, gx, gy, gyaw, gv, ga, max_accel, max_jerk, dt): """ quinic polynomial planner @@ -81,6 +86,7 @@ def quinic_polynomials_planner(sx, sy, syaw, sv, sa, gx, gy, gyaw, gv, ga, max_a gyaw: goal yaw angle [rad] ga: goal accel [m/ss] max_accel: maximum accel [m/ss] + max_jerk: maximum jerk [m/sss] dt: time tick [s] return @@ -107,7 +113,7 @@ def quinic_polynomials_planner(sx, sy, syaw, sv, sa, gx, gy, gyaw, gv, ga, max_a xqp = quinic_polynomial(sx, vxs, axs, gx, vxg, axg, T) yqp = quinic_polynomial(sy, vys, ays, gy, vyg, ayg, T) - time, rx, ry, ryaw, rv, ra = [], [], [], [], [], [] + time, rx, ry, ryaw, rv, ra, rj = [], [], [], [], [], [], [] for t in np.arange(0.0, T + dt, dt): time.append(t) @@ -126,10 +132,16 @@ def quinic_polynomials_planner(sx, sy, syaw, sv, sa, gx, gy, gyaw, gv, ga, max_a a = np.hypot(ax, ay) if len(rv) >= 2 and rv[-1] - rv[-2] < 0.0: a *= -1 - pass ra.append(a) - if max([abs(i) for i in ra]) <= max_accel: + jx = xqp.calc_third_derivative(t) + jy = yqp.calc_third_derivative(t) + j = np.hypot(jx, jy) + if len(ra) >= 2 and ra[-1] - ra[-2] < 0.0: + j *= -1 + rj.append(j) + + if max([abs(i) for i in ra]) <= max_accel and max([abs(i) for i in rj]) <= max_jerk: print("find path!!") break @@ -143,14 +155,16 @@ def quinic_polynomials_planner(sx, sy, syaw, sv, sa, gx, gy, gyaw, gv, ga, max_a plot_arrow(rx[i], ry[i], ryaw[i]) plt.title("Time[s]:" + str(time[i])[0:4] + " v[m/s]:" + str(rv[i])[0:4] + - " a[m/ss]:" + str(ra[i])[0:4]) + " a[m/ss]:" + str(ra[i])[0:4] + + " jerk[m/sss]:" + str(rj[i])[0:4], + ) plt.pause(0.001) - return time, rx, ry, ryaw, rv, ra + return time, rx, ry, ryaw, rv, ra, rj def plot_arrow(x, y, yaw, length=1.0, width=0.5, fc="r", ec="k"): - u""" + """ Plot arrow """ @@ -177,10 +191,11 @@ def main(): gv = 1.0 # goal speed [m/s] ga = 0.1 # goal accel [m/ss] max_accel = 1.0 # max accel [m/ss] + max_jerk = 0.5 # max jerk [m/sss] dt = 0.1 # time tick [s] - time, x, y, yaw, v, a = quinic_polynomials_planner( - sx, sy, syaw, sv, sa, gx, gy, gyaw, gv, ga, max_accel, dt) + time, x, y, yaw, v, a, j = quinic_polynomials_planner( + sx, sy, syaw, sv, sa, gx, gy, gyaw, gv, ga, max_accel, max_jerk, dt) if show_animation: plt.plot(x, y, "-r") @@ -203,6 +218,12 @@ def main(): plt.ylabel("accel[m/ss]") plt.grid(True) + plt.subplots() + plt.plot(time, j, "-r") + plt.xlabel("Time[s]") + plt.ylabel("jerk[m/sss]") + plt.grid(True) + plt.show() diff --git a/PathPlanning/RRT/simple_rrt.py b/PathPlanning/RRT/simple_rrt.py index ef28ee03..0d801b27 100644 --- a/PathPlanning/RRT/simple_rrt.py +++ b/PathPlanning/RRT/simple_rrt.py @@ -39,7 +39,7 @@ class RRT(): self.obstacleList = obstacleList def Planning(self, animation=True): - u""" + """ Pathplanning animation: flag for animation on or off @@ -71,6 +71,7 @@ class RRT(): continue self.nodeList.append(newNode) + print("nNodelist:", len(self.nodeList)) # check goal dx = newNode.x - self.end.x @@ -94,7 +95,7 @@ class RRT(): return path def DrawGraph(self, rnd=None): - u""" + """ Draw Graph """ plt.clf() @@ -133,7 +134,7 @@ class RRT(): class Node(): - u""" + """ RRT Node """ @@ -144,7 +145,7 @@ class Node(): def main(): - print("start RRT path planning") + print("start simple RRT path planning") # ====Search Path with RRT==== obstacleList = [ diff --git a/PathPlanning/RRTDubins/dubins_path_planning.py b/PathPlanning/RRTDubins/dubins_path_planning.py index a5bc8bbd..b489ca5f 100644 --- a/PathPlanning/RRTDubins/dubins_path_planning.py +++ b/PathPlanning/RRTDubins/dubins_path_planning.py @@ -17,13 +17,7 @@ def mod2pi(theta): def pi_2_pi(angle): - while(angle >= math.pi): - angle = angle - 2.0 * math.pi - - while(angle <= -math.pi): - angle = angle + 2.0 * math.pi - - return angle + return (angle + math.pi) % (2*math.pi) - math.pi def LSL(alpha, beta, d): diff --git a/PathPlanning/RRTDubins/rrt_dubins.py b/PathPlanning/RRTDubins/rrt_dubins.py index 71c84d1b..33126335 100644 --- a/PathPlanning/RRTDubins/rrt_dubins.py +++ b/PathPlanning/RRTDubins/rrt_dubins.py @@ -96,14 +96,10 @@ class RRT(): return newNode + def pi_2_pi(self, angle): - while(angle >= math.pi): - angle = angle - 2.0 * math.pi + return (angle + math.pi) % (2*math.pi) - math.pi - while(angle <= -math.pi): - angle = angle + 2.0 * math.pi - - return angle def steer(self, rnd, nind): # print(rnd) diff --git a/PathPlanning/RRTStarDubins/dubins_path_planning.py b/PathPlanning/RRTStarDubins/dubins_path_planning.py index 92565c6e..ad9e7e55 100644 --- a/PathPlanning/RRTStarDubins/dubins_path_planning.py +++ b/PathPlanning/RRTStarDubins/dubins_path_planning.py @@ -13,13 +13,7 @@ def mod2pi(theta): def pi_2_pi(angle): - while(angle >= math.pi): - angle = angle - 2.0 * math.pi - - while(angle <= -math.pi): - angle = angle + 2.0 * math.pi - - return angle + return (angle + math.pi) % (2*math.pi) - math.pi def LSL(alpha, beta, d): diff --git a/PathPlanning/RRTStarDubins/rrt_star_dubins.py b/PathPlanning/RRTStarDubins/rrt_star_dubins.py index fdc659e6..79aabf70 100644 --- a/PathPlanning/RRTStarDubins/rrt_star_dubins.py +++ b/PathPlanning/RRTStarDubins/rrt_star_dubins.py @@ -97,13 +97,7 @@ class RRT(): return newNode def pi_2_pi(self, angle): - while(angle >= math.pi): - angle = angle - 2.0 * math.pi - - while(angle <= -math.pi): - angle = angle + 2.0 * math.pi - - return angle + return (angle + math.pi) % (2*math.pi) - math.pi def steer(self, rnd, nind): # print(rnd) diff --git a/PathPlanning/RRTStarReedsShepp/rrt_star_reeds_shepp.py b/PathPlanning/RRTStarReedsShepp/rrt_star_reeds_shepp.py index e4cab301..089915bb 100644 --- a/PathPlanning/RRTStarReedsShepp/rrt_star_reeds_shepp.py +++ b/PathPlanning/RRTStarReedsShepp/rrt_star_reeds_shepp.py @@ -106,13 +106,7 @@ class RRT(): return newNode def pi_2_pi(self, angle): - while(angle > math.pi): - angle = angle - 2.0 * math.pi - - while(angle < -math.pi): - angle = angle + 2.0 * math.pi - - return angle + return (angle + math.pi) % (2*math.pi) - math.pi def steer(self, rnd, nind): diff --git a/PathPlanning/ReedsSheppPath/reeds_shepp_path_planning.py b/PathPlanning/ReedsSheppPath/reeds_shepp_path_planning.py index 322accbb..28e4ef9d 100644 --- a/PathPlanning/ReedsSheppPath/reeds_shepp_path_planning.py +++ b/PathPlanning/ReedsSheppPath/reeds_shepp_path_planning.py @@ -331,13 +331,7 @@ def generate_local_course(L, lengths, mode, maxc, step_size): def pi_2_pi(angle): - while(angle > math.pi): - angle = angle - 2.0 * math.pi - - while(angle < -math.pi): - angle = angle + 2.0 * math.pi - - return angle + return (angle + math.pi) % (2*math.pi) - math.pi def calc_paths(sx, sy, syaw, gx, gy, gyaw, maxc, step_size): diff --git a/PathTracking/lqr_speed_steer_control/lqr_speed_steer_control.py b/PathTracking/lqr_speed_steer_control/lqr_speed_steer_control.py index 50331b3f..6468737d 100644 --- a/PathTracking/lqr_speed_steer_control/lqr_speed_steer_control.py +++ b/PathTracking/lqr_speed_steer_control/lqr_speed_steer_control.py @@ -51,13 +51,7 @@ def update(state, a, delta): def pi_2_pi(angle): - while (angle > math.pi): - angle = angle - 2.0 * math.pi - - while (angle < -math.pi): - angle = angle + 2.0 * math.pi - - return angle + return (angle + math.pi) % (2*math.pi) - math.pi def solve_DARE(A, B, Q, R): @@ -148,12 +142,14 @@ def calc_nearest_index(state, cx, cy, cyaw): dx = [state.x - icx for icx in cx] dy = [state.y - icy for icy in cy] - d = [abs(math.sqrt(idx ** 2 + idy ** 2)) for (idx, idy) in zip(dx, dy)] + d = [idx ** 2 + idy ** 2 for (idx, idy) in zip(dx, dy)] mind = min(d) ind = d.index(mind) + mind = math.sqrt(mind) + dxl = cx[ind] - state.x dyl = cy[ind] - state.y diff --git a/PathTracking/lqr_steer_control/lqr_steer_control.py b/PathTracking/lqr_steer_control/lqr_steer_control.py index 341bb278..c6f87aef 100644 --- a/PathTracking/lqr_steer_control/lqr_steer_control.py +++ b/PathTracking/lqr_steer_control/lqr_steer_control.py @@ -61,13 +61,7 @@ def PIDControl(target, current): def pi_2_pi(angle): - while (angle > math.pi): - angle = angle - 2.0 * math.pi - - while (angle < -math.pi): - angle = angle + 2.0 * math.pi - - return angle + return (angle + math.pi) % (2*math.pi) - math.pi def solve_DARE(A, B, Q, R): @@ -146,12 +140,14 @@ def calc_nearest_index(state, cx, cy, cyaw): dx = [state.x - icx for icx in cx] dy = [state.y - icy for icy in cy] - d = [abs(math.sqrt(idx ** 2 + idy ** 2)) for (idx, idy) in zip(dx, dy)] + d = [idx ** 2 + idy ** 2 for (idx, idy) in zip(dx, dy)] mind = min(d) ind = d.index(mind) + mind = math.sqrt(mind) + dxl = cx[ind] - state.x dyl = cy[ind] - state.y diff --git a/PathTracking/model_predictive_speed_and_steer_control/model_predictive_speed_and_steer_control.py b/PathTracking/model_predictive_speed_and_steer_control/model_predictive_speed_and_steer_control.py index 0028c0a8..4272f4bd 100644 --- a/PathTracking/model_predictive_speed_and_steer_control/model_predictive_speed_and_steer_control.py +++ b/PathTracking/model_predictive_speed_and_steer_control/model_predictive_speed_and_steer_control.py @@ -88,16 +88,16 @@ def get_linear_model_matrix(v, phi, delta): A[0, 3] = - DT * v * math.sin(phi) A[1, 2] = DT * math.sin(phi) A[1, 3] = DT * v * math.cos(phi) - A[3, 2] = DT * math.tan(delta) + A[3, 2] = DT * math.tan(delta) / WB B = np.matrix(np.zeros((NX, NU))) B[2, 0] = DT B[3, 1] = DT * v / (WB * math.cos(delta) ** 2) - C = np.matrix(np.zeros((NX, 1))) - C[0, 0] = DT * v * math.sin(phi) * phi - C[1, 0] = - DT * v * math.cos(phi) * phi - C[3, 0] = v * delta / (WB * math.cos(delta) ** 2) + C = np.zeros(NX) + C[0] = DT * v * math.sin(phi) * phi + C[1] = - DT * v * math.cos(phi) * phi + C[3] = v * delta / (WB * math.cos(delta) ** 2) return A, B, C @@ -188,20 +188,22 @@ def calc_nearest_index(state, cx, cy, cyaw, pind): dx = [state.x - icx for icx in cx[pind:(pind + N_IND_SEARCH)]] dy = [state.y - icy for icy in cy[pind:(pind + N_IND_SEARCH)]] - d = [abs(math.sqrt(idx ** 2 + idy ** 2)) for (idx, idy) in zip(dx, dy)] + d = [idx ** 2 + idy ** 2 for (idx, idy) in zip(dx, dy)] - min_d = min(d) + mind = min(d) - ind = d.index(min_d) + pind + ind = d.index(mind) + pind + + mind = math.sqrt(mind) dxl = cx[ind] - state.x dyl = cy[ind] - state.y angle = pi_2_pi(cyaw[ind] - math.atan2(dyl, dxl)) if angle < 0: - min_d *= -1 + mind *= -1 - return ind, min_d + return ind, mind def predict_motion(x0, oa, od, xref): @@ -252,8 +254,8 @@ def linear_mpc_control(xref, xbar, x0, dref): dref: reference steer angle """ - x = cvxpy.Variable(NX, T + 1) - u = cvxpy.Variable(NU, T) + x = cvxpy.Variable((NX, T + 1)) + u = cvxpy.Variable((NU, T)) cost = 0.0 constraints = [] @@ -271,18 +273,18 @@ def linear_mpc_control(xref, xbar, x0, dref): if t < (T - 1): cost += cvxpy.quad_form(u[:, t + 1] - u[:, t], Rd) constraints += [cvxpy.abs(u[1, t + 1] - u[1, t]) - < MAX_DSTEER * DT] + <= MAX_DSTEER * DT] cost += cvxpy.quad_form(xref[:, T] - x[:, T], Qf) constraints += [x[:, 0] == x0] constraints += [x[2, :] <= MAX_SPEED] constraints += [x[2, :] >= MIN_SPEED] - constraints += [cvxpy.abs(u[0, :]) < MAX_ACCEL] - constraints += [cvxpy.abs(u[1, :]) < MAX_STEER] + constraints += [cvxpy.abs(u[0, :]) <= MAX_ACCEL] + constraints += [cvxpy.abs(u[1, :]) <= MAX_STEER] prob = cvxpy.Problem(cvxpy.Minimize(cost), constraints) - prob.solve(verbose=False) + prob.solve(solver=cvxpy.ECOS, verbose=False) if prob.status == cvxpy.OPTIMAL or prob.status == cvxpy.OPTIMAL_INACCURATE: ox = get_nparray_from_matrix(x.value[0, :]) @@ -363,7 +365,7 @@ def check_goal(state, goal, tind, nind): return False -def do_simulation(cx, cy, cyaw, ck, sp, dl): +def do_simulation(cx, cy, cyaw, ck, sp, dl, initial_state): """ Simulation @@ -377,7 +379,14 @@ def do_simulation(cx, cy, cyaw, ck, sp, dl): """ goal = [cx[-1], cy[-1]] - state = State(x=cx[0], y=cy[0], yaw=cyaw[0], v=0.0) + + state = initial_state + + # initial yaw compensation + if state.yaw - cyaw[0] >= math.pi: + state.yaw -= math.pi * 2.0 + elif state.yaw - cyaw[0] <= -math.pi: + state.yaw += math.pi * 2.0 time = 0.0 x = [state.x] @@ -471,9 +480,11 @@ def smooth_yaw(yaw): for i in range(len(yaw) - 1): dyaw = yaw[i + 1] - yaw[i] + while dyaw >= math.pi / 2.0: yaw[i + 1] -= math.pi * 2.0 dyaw = yaw[i + 1] - yaw[i] + while dyaw <= -math.pi / 2.0: yaw[i + 1] += math.pi * 2.0 dyaw = yaw[i + 1] - yaw[i] @@ -481,6 +492,35 @@ def smooth_yaw(yaw): return yaw +def get_straight_course(dl): + ax = [0.0, 5.0, 10.0, 20.0, 30.0, 40.0, 50.0] + ay = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + cx, cy, cyaw, ck, s = cubic_spline_planner.calc_spline_course( + ax, ay, ds=dl) + + return cx, cy, cyaw, ck + + +def get_straight_course2(dl): + ax = [0.0, -10.0, -20.0, -40.0, -50.0, -60.0, -70.0] + ay = [0.0, -1.0, 1.0, 0.0, -1.0, 1.0, 0.0] + cx, cy, cyaw, ck, s = cubic_spline_planner.calc_spline_course( + ax, ay, ds=dl) + + return cx, cy, cyaw, ck + + +def get_straight_course3(dl): + ax = [0.0, -10.0, -20.0, -40.0, -50.0, -60.0, -70.0] + ay = [0.0, -1.0, 1.0, 0.0, -1.0, 1.0, 0.0] + cx, cy, cyaw, ck, s = cubic_spline_planner.calc_spline_course( + ax, ay, ds=dl) + + cyaw = [i - math.pi for i in cyaw] + + return cx, cy, cyaw, ck + + def get_forward_course(dl): ax = [0.0, 60.0, 125.0, 50.0, 75.0, 30.0, -10.0] ay = [0.0, 0.0, 50.0, 65.0, 30.0, 50.0, -20.0] @@ -512,12 +552,51 @@ def main(): print(__file__ + " start!!") dl = 1.0 # course tick - # cx, cy, cyaw, ck = get_forward_course(dl) - cx, cy, cyaw, ck = get_switch_back_course(dl) + # cx, cy, cyaw, ck = get_straight_course(dl) + # cx, cy, cyaw, ck = get_straight_course2(dl) + cx, cy, cyaw, ck = get_straight_course3(dl) + # cx, cy, cyaw, ck = get_forward_course(dl) + # CX, cy, cyaw, ck = get_switch_back_course(dl) sp = calc_speed_profile(cx, cy, cyaw, TARGET_SPEED) - t, x, y, yaw, v, d, a = do_simulation(cx, cy, cyaw, ck, sp, dl) + initial_state = State(x=cx[0], y=cy[0], yaw=cyaw[0], v=0.0) + + t, x, y, yaw, v, d, a = do_simulation( + cx, cy, cyaw, ck, sp, dl, initial_state) + + if show_animation: + plt.close("all") + plt.subplots() + plt.plot(cx, cy, "-r", label="spline") + plt.plot(x, y, "-g", label="tracking") + plt.grid(True) + plt.axis("equal") + plt.xlabel("x[m]") + plt.ylabel("y[m]") + plt.legend() + + plt.subplots() + plt.plot(t, v, "-r", label="speed") + plt.grid(True) + plt.xlabel("Time [s]") + plt.ylabel("Speed [kmh]") + + plt.show() + + +def main2(): + print(__file__ + " start!!") + + dl = 1.0 # course tick + cx, cy, cyaw, ck = get_straight_course3(dl) + + sp = calc_speed_profile(cx, cy, cyaw, TARGET_SPEED) + + initial_state = State(x=cx[0], y=cy[0], yaw=0.0, v=0.0) + + t, x, y, yaw, v, d, a = do_simulation( + cx, cy, cyaw, ck, sp, dl, initial_state) if show_animation: plt.close("all") @@ -540,4 +619,5 @@ def main(): if __name__ == '__main__': - main() + # main() + main2() diff --git a/PathTracking/model_predictive_speed_and_steer_control/notebook.ipynb b/PathTracking/model_predictive_speed_and_steer_control/notebook.ipynb new file mode 100644 index 00000000..bf91aad7 --- /dev/null +++ b/PathTracking/model_predictive_speed_and_steer_control/notebook.ipynb @@ -0,0 +1,301 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Model predictive speed and steering control\n", + "\n", + "code:\n", + "\n", + "https://github.com/AtsushiSakai/PythonRobotics/blob/master/PathTracking/model_predictive_speed_and_steer_control/model_predictive_speed_and_steer_control.py\n", + "\n", + "This is a path tracking simulation using model predictive control (MPC).\n", + "\n", + "The MPC controller controls vehicle speed and steering base on linealized model.\n", + "\n", + "This code uses cvxpy as an optimization modeling tool.\n", + "\n", + "- [Welcome to CVXPY 1\\.0 — CVXPY 1\\.0\\.6 documentation](http://www.cvxpy.org/)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# MPC modeling\n", + "\n", + "State vector is:\n", + "$$ z = [x, y, v,\\phi]$$ x: x-position, y:y-position, v:velocity, φ: yaw angle\n", + "\n", + "Input vector is:\n", + "$$ u = [a, \\delta]$$ a: accellation, δ: steering angle\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The MPC cotroller minimize this cost function for path tracking:\n", + "\n", + "$$min\\ Q_f(z_{T,ref}-z_{T})^2+Q\\Sigma({z_{t,ref}-z_{t}})^2+R\\Sigma{u_t}^2+R_d\\Sigma({u_{t+1}-u_{t}})^2$$\n", + "\n", + "z_ref come from target path and speed." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "subject to:\n", + "- Linearlied vehicle model\n", + "$$z_{t+1}=Az_t+Bu+C$$\n", + "- Maximum steering speed\n", + "$$|u_{t+1}-u_{t}| 0.001: x_traj.append(x) y_traj.append(y) @@ -54,63 +50,80 @@ def move_to_pose(x_start, y_start, theta_start, x_goal, y_goal, theta_goal): """ Restrict alpha and beta (angle differences) to the range - [-pi, pi] to prevent unstable behavior e.g. difference going + [-pi, pi] to prevent unstable behavior e.g. difference going from 0 rad to 2*pi rad with slight turn - """ + """ - rho = sqrt(x_diff**2 + y_diff**2) - alpha = (atan2(y_diff, x_diff) - theta + pi)%(2*pi) - pi - beta = (theta_goal - theta - alpha + pi)%(2*pi) - pi + rho = np.sqrt(x_diff**2 + y_diff**2) + alpha = (np.arctan2(y_diff, x_diff) - + theta + np.pi) % (2 * np.pi) - np.pi + beta = (theta_goal - theta - alpha + np.pi) % (2 * np.pi) - np.pi - v = Kp_rho*rho - w = Kp_alpha*alpha + Kp_beta*beta + v = Kp_rho * rho + w = Kp_alpha * alpha + Kp_beta * beta - if alpha > pi/2 or alpha < -pi/2: + if alpha > np.pi / 2 or alpha < -np.pi / 2: v = -v - theta = theta + w*dt - x = x + v*cos(theta)*dt - y = y + v*sin(theta)*dt + theta = theta + w * dt + x = x + v * np.cos(theta) * dt + y = y + v * np.sin(theta) * dt - plot_vehicle(x, y, theta, x_traj, y_traj) + if show_animation: + plt.cla() + plt.arrow(x_start, y_start, np.cos(theta_start), + np.sin(theta_start), color='r', width=0.1) + plt.arrow(x_goal, y_goal, np.cos(theta_goal), + np.sin(theta_goal), color='g', width=0.1) + plot_vehicle(x, y, theta, x_traj, y_traj) def plot_vehicle(x, y, theta, x_traj, y_traj): + # Corners of triangular vehicle when pointing to the right (0 radians) + p1_i = np.array([0.5, 0, 1]).T + p2_i = np.array([-0.5, 0.25, 1]).T + p3_i = np.array([-0.5, -0.25, 1]).T + T = transformation_matrix(x, y, theta) p1 = np.matmul(T, p1_i) p2 = np.matmul(T, p2_i) p3 = np.matmul(T, p3_i) - plt.cla() - plt.plot([p1[0], p2[0]], [p1[1], p2[1]], 'k-') plt.plot([p2[0], p3[0]], [p2[1], p3[1]], 'k-') plt.plot([p3[0], p1[0]], [p3[1], p1[1]], 'k-') - plt.arrow(x_start, y_start, cos(theta_start), sin(theta_start), color='r', width=0.1) - plt.arrow(x_goal, y_goal, cos(theta_goal), sin(theta_goal), color='g', width=0.1) plt.plot(x_traj, y_traj, 'b--') plt.xlim(0, 20) plt.ylim(0, 20) - plt.show() plt.pause(dt) + def transformation_matrix(x, y, theta): return np.array([ - [cos(theta), -sin(theta), x], - [sin(theta), cos(theta), y], + [np.cos(theta), -np.sin(theta), x], + [np.sin(theta), np.cos(theta), y], [0, 0, 1] - ]) + ]) + + +def main(): + + for i in range(5): + x_start = 20 * random() + y_start = 20 * random() + theta_start = 2 * np.pi * random() - np.pi + x_goal = 20 * random() + y_goal = 20 * random() + theta_goal = 2 * np.pi * random() - np.pi + print("Initial x: %.2f m\nInitial y: %.2f m\nInitial theta: %.2f rad\n" % + (x_start, y_start, theta_start)) + print("Goal x: %.2f m\nGoal y: %.2f m\nGoal theta: %.2f rad\n" % + (x_goal, y_goal, theta_goal)) + move_to_pose(x_start, y_start, theta_start, x_goal, y_goal, theta_goal) + if __name__ == '__main__': - x_start = 20*random() - y_start = 20*random() - theta_start = 2*pi*random() - pi - x_goal = 20*random() - y_goal = 20*random() - theta_goal = 2*pi*random() - pi - print("Initial x: %.2f m\nInitial y: %.2f m\nInitial theta: %.2f rad\n" % (x_start, y_start, theta_start)) - print("Goal x: %.2f m\nGoal y: %.2f m\nGoal theta: %.2f rad\n" % (x_goal, y_goal, theta_goal)) - move_to_pose(x_start, y_start, theta_start, x_goal, y_goal, theta_goal) \ No newline at end of file + main() diff --git a/PathTracking/rear_wheel_feedback/pycubicspline b/PathTracking/rear_wheel_feedback/pycubicspline deleted file mode 160000 index 34b741f7..00000000 --- a/PathTracking/rear_wheel_feedback/pycubicspline +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 34b741f7eee6cfbe851317ca36dab59fc560ba9a diff --git a/PathTracking/rear_wheel_feedback/rear_wheel_feedback.py b/PathTracking/rear_wheel_feedback/rear_wheel_feedback.py index 486deb6c..19dff265 100644 --- a/PathTracking/rear_wheel_feedback/rear_wheel_feedback.py +++ b/PathTracking/rear_wheel_feedback/rear_wheel_feedback.py @@ -83,12 +83,14 @@ def calc_nearest_index(state, cx, cy, cyaw): dx = [state.x - icx for icx in cx] dy = [state.y - icy for icy in cy] - d = [abs(math.sqrt(idx ** 2 + idy ** 2)) for (idx, idy) in zip(dx, dy)] + d = [idx ** 2 + idy ** 2 for (idx, idy) in zip(dx, dy)] mind = min(d) ind = d.index(mind) + mind = math.sqrt(mind) + dxl = cx[ind] - state.x dyl = cy[ind] - state.y diff --git a/PathTracking/stanley_controller/pycubicspline b/PathTracking/stanley_controller/pycubicspline deleted file mode 160000 index 34b741f7..00000000 --- a/PathTracking/stanley_controller/pycubicspline +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 34b741f7eee6cfbe851317ca36dab59fc560ba9a diff --git a/PathTracking/stanley_controller/stanley_controller.py b/PathTracking/stanley_controller/stanley_controller.py index f287bef4..1ff9192d 100644 --- a/PathTracking/stanley_controller/stanley_controller.py +++ b/PathTracking/stanley_controller/stanley_controller.py @@ -5,99 +5,144 @@ Path tracking simulation with Stanley steering control and PID speed control. author: Atsushi Sakai (@Atsushi_twi) """ +from __future__ import division, print_function + import sys + sys.path.append("../../PathPlanning/CubicSpline/") -import math import matplotlib.pyplot as plt -import cubic_spline_planner +import numpy as np +import cubic_spline_planner k = 0.5 # control gain Kp = 1.0 # speed propotional gain dt = 0.1 # [s] time difference L = 2.9 # [m] Wheel base of vehicle -max_steer = math.radians(30.0) # [rad] max steering angle +max_steer = np.radians(30.0) # [rad] max steering angle show_animation = True -class State: +class State(object): + """ + Class representing the state of a vehicle. + + :param x: (float) x-coordinate + :param y: (float) y-coordinate + :param yaw: (float) yaw angle + :param v: (float) speed + """ def __init__(self, x=0.0, y=0.0, yaw=0.0, v=0.0): + """Instantiate the object.""" + super(State, self).__init__() self.x = x self.y = y self.yaw = yaw self.v = v + def update(self, acceleration, delta): + """ + Update the state of the vehicle. -def update(state, a, delta): + Stanley Control uses bicycle model. - if delta >= max_steer: - delta = max_steer - elif delta <= -max_steer: - delta = -max_steer + :param acceleration: (float) Acceleration + :param delta: (float) Steering + """ + delta = np.clip(delta, -max_steer, max_steer) - state.x = state.x + state.v * math.cos(state.yaw) * dt - state.y = state.y + state.v * math.sin(state.yaw) * dt - state.yaw = state.yaw + state.v / L * math.tan(delta) * dt - state.yaw = pi_2_pi(state.yaw) - state.v = state.v + a * dt - - return state + self.x += self.v * np.cos(self.yaw) * dt + self.y += self.v * np.sin(self.yaw) * dt + self.yaw += self.v / L * np.tan(delta) * dt + self.yaw = normalize_angle(self.yaw) + self.v += acceleration * dt -def PIDControl(target, current): - a = Kp * (target - current) +def pid_control(target, current): + """ + Proportional control for the speed. - return a + :param target: (float) + :param current: (float) + :return: (float) + """ + return Kp * (target - current) -def stanley_control(state, cx, cy, cyaw, pind): +def stanley_control(state, cx, cy, cyaw, last_target_idx): + """ + Stanley steering control. - ind, efa = calc_target_index(state, cx, cy) + :param state: (State object) + :param cx: ([float]) + :param cy: ([float]) + :param cyaw: ([float]) + :param last_target_idx: (int) + :return: (float, int) + """ + current_target_idx, error_front_axle = calc_target_index(state, cx, cy) - if pind >= ind: - ind = pind + if last_target_idx >= current_target_idx: + current_target_idx = last_target_idx - theta_e = pi_2_pi(cyaw[ind] - state.yaw) - theta_d = math.atan2(k * efa, state.v) + # theta_e corrects the heading error + theta_e = normalize_angle(cyaw[current_target_idx] - state.yaw) + # theta_d corrects the cross track error + theta_d = np.arctan2(k * error_front_axle, state.v) + # Steering control delta = theta_e + theta_d - return delta, ind + return delta, current_target_idx -def pi_2_pi(angle): - while (angle > math.pi): - angle = angle - 2.0 * math.pi +def normalize_angle(angle): + """ + Normalize an angle to [-pi, pi]. - while (angle < -math.pi): - angle = angle + 2.0 * math.pi + :param angle: (float) + :return: (float) Angle in radian in [-pi, pi] + """ + while angle > np.pi: + angle -= 2.0 * np.pi + + while angle < -np.pi: + angle += 2.0 * np.pi return angle def calc_target_index(state, cx, cy): + """ + Compute index in the trajectory list of the target. - # calc frant axle position - fx = state.x + L * math.cos(state.yaw) - fy = state.y + L * math.sin(state.yaw) + :param state: (State object) + :param cx: [float] + :param cy: [float] + :return: (int, float) + """ + # Calc front axle position + fx = state.x + L * np.cos(state.yaw) + fy = state.y + L * np.sin(state.yaw) - # search nearest point index + # Search nearest point index dx = [fx - icx for icx in cx] dy = [fy - icy for icy in cy] - d = [math.sqrt(idx ** 2 + idy ** 2) for (idx, idy) in zip(dx, dy)] - mind = min(d) - ind = d.index(mind) + d = [np.sqrt(idx ** 2 + idy ** 2) for (idx, idy) in zip(dx, dy)] + error_front_axle = min(d) + target_idx = d.index(error_front_axle) - tyaw = pi_2_pi(math.atan2(fy - cy[ind], fx - cx[ind]) - state.yaw) - if tyaw > 0.0: - mind = - mind + target_yaw = normalize_angle(np.arctan2(fy - cy[target_idx], fx - cx[target_idx]) - state.yaw) + if target_yaw > 0.0: + error_front_axle = - error_front_axle - return ind, mind + return target_idx, error_front_axle def main(): + """Plot an example of Stanley steering control on a cubic spline.""" # target course ax = [0.0, 100.0, 100.0, 50.0, 60.0] ay = [0.0, 0.0, -30.0, -20.0, 0.0] @@ -107,26 +152,26 @@ def main(): target_speed = 30.0 / 3.6 # [m/s] - T = 100.0 # max simulation time + max_simulation_time = 100.0 - # initial state - state = State(x=-0.0, y=5.0, yaw=math.radians(20.0), v=0.0) + # Initial state + state = State(x=-0.0, y=5.0, yaw=np.radians(20.0), v=0.0) - lastIndex = len(cx) - 1 + last_idx = len(cx) - 1 time = 0.0 x = [state.x] y = [state.y] yaw = [state.yaw] v = [state.v] t = [0.0] - target_ind, mind = calc_target_index(state, cx, cy) + target_idx, _ = calc_target_index(state, cx, cy) - while T >= time and lastIndex > target_ind: - ai = PIDControl(target_speed, state.v) - di, target_ind = stanley_control(state, cx, cy, cyaw, target_ind) - state = update(state, ai, di) + while max_simulation_time >= time and last_idx > target_idx: + ai = pid_control(target_speed, state.v) + di, target_idx = stanley_control(state, cx, cy, cyaw, target_idx) + state.update(ai, di) - time = time + dt + time += dt x.append(state.x) y.append(state.y) @@ -138,14 +183,14 @@ def main(): plt.cla() plt.plot(cx, cy, ".r", label="course") plt.plot(x, y, "-b", label="trajectory") - plt.plot(cx[target_ind], cy[target_ind], "xg", label="target") + plt.plot(cx[target_idx], cy[target_idx], "xg", label="target") plt.axis("equal") plt.grid(True) plt.title("Speed[km/h]:" + str(state.v * 3.6)[:4]) plt.pause(0.001) # Test - assert lastIndex >= target_ind, "Cannot goal" + assert last_idx >= target_idx, "Cannot reach goal" if show_animation: plt.plot(cx, cy, ".r", label="course") diff --git a/README.md b/README.md index 73dad56d..b6254833 100644 --- a/README.md +++ b/README.md @@ -2,61 +2,78 @@ # PythonRobotics [![Build Status](https://travis-ci.org/AtsushiSakai/PythonRobotics.svg?branch=master)](https://travis-ci.org/AtsushiSakai/PythonRobotics) +[![Documentation Status](https://readthedocs.org/projects/pythonrobotics/badge/?version=latest)](https://pythonrobotics.readthedocs.io/en/latest/?badge=latest) +[![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/AtsushiSakai) Python codes for robotics algorithm. + # Table of Contents + * [What is this?](#what-is-this) * [Requirements](#requirements) + * [Documentation](#documentation) * [How to use](#how-to-use) * [Localization](#localization) * [Extended Kalman Filter localization](#extended-kalman-filter-localization) - * [Unscented Kalman Filter localization](#unscented-kalman-filter-localization) - * [Particle Filter localization](#particle-filter-localization) + * [Particle filter localization](#particle-filter-localization) + * [Histogram filter localization](#histogram-filter-localization) * [Mapping](#mapping) * [Gaussian grid map](#gaussian-grid-map) * [Ray casting grid map](#ray-casting-grid-map) + * [k-means object clustering](#k-means-object-clustering) * [SLAM](#slam) * [Iterative Closest Point (ICP) Matching](#iterative-closest-point-icp-matching) - * [EKF SLAM](#ekf-slam) + * [FastSLAM 1.0](#fastslam-10) + * [Graph based SLAM](#graph-based-slam) * [Path Planning](#path-planning) * [Dynamic Window Approach](#dynamic-window-approach) * [Grid based search](#grid-based-search) * [Dijkstra algorithm](#dijkstra-algorithm) * [A* algorithm](#a-algorithm) * [Potential Field algorithm](#potential-field-algorithm) - * [Model Predictive Trajectory Generator](#model-predictive-trajectory-generator) - * [Path optimization sample](#path-optimization-sample) - * [Lookup table generation sample](#lookup-table-generation-sample) * [State Lattice Planning](#state-lattice-planning) - * [Uniform polar sampling](#uniform-polar-sampling) * [Biased polar sampling](#biased-polar-sampling) * [Lane sampling](#lane-sampling) * [Probabilistic Road-Map (PRM) planning](#probabilistic-road-map-prm-planning) - * [Voronoi Road-Map planning](#voronoi-road-map-planning) * [Rapidly-Exploring Random Trees (RRT)](#rapidly-exploring-random-trees-rrt) - * [Basic RRT](#basic-rrt) * [RRT*](#rrt) - * [RRT with dubins path](#rrt-with-dubins-path) - * [RRT* with dubins path](#rrt-with-dubins-path-1) * [RRT* with reeds-sheep path](#rrt-with-reeds-sheep-path) - * [Closed Loop RRT*](#closed-loop-rrt) - * [Cubic spline planning](#cubic-spline-planning) - * [B-Spline planning](#b-spline-planning) - * [Bezier path planning](#bezier-path-planning) + * [LQR-RRT*](#lqr-rrt) * [Quintic polynomials planning](#quintic-polynomials-planning) - * [Dubins path planning](#dubins-path-planning) * [Reeds Shepp planning](#reeds-shepp-planning) + * [LQR based path planning](#lqr-based-path-planning) * [Optimal Trajectory in a Frenet Frame](#optimal-trajectory-in-a-frenet-frame) * [Path tracking](#path-tracking) - * [Pure pursuit tracking](#pure-pursuit-tracking) + * [move to a pose control](#move-to-a-pose-control) * [Stanley control](#stanley-control) * [Rear wheel feedback control](#rear-wheel-feedback-control) - * [Linear–quadratic regulator (LQR) steering control](#linearquadratic-regulator-lqr-steering-control) * [Linear–quadratic regulator (LQR) speed and steering control](#linearquadratic-regulator-lqr-speed-and-steering-control) * [Model predictive speed and steering control](#model-predictive-speed-and-steering-control) + * [Arm Navigation](#arm-navigation) + * [N joint arm to point control](#n-joint-arm-to-point-control) + * [Aerial Navigation](#aerial-navigation) + * [drone 3d trajectory following](#drone-3d-trajectory-following) * [License](#license) * [Contribution](#contribution) - * [Author](#author) + * [Support](#support) + * [Authors](#authors) + +# What is this? + +This is a Python code collection of robotics algorithms, especially for autonomous navigation. + +Features: + +1. Easy to read for understanding each algorithm's basic idea. + +2. Widely used and practical algorithms are selected. + +3. Minimum dependency. + +See this paper for more details: + +- [\[1808\.10703\] PythonRobotics: a Python code collection of robotics algorithms](https://arxiv.org/abs/1808.10703) ([BibTeX](https://github.com/AtsushiSakai/PythonRoboticsPaper/blob/master/python_robotics.bib)) + # Requirements @@ -70,11 +87,17 @@ Python codes for robotics algorithm. - pandas -- [cvxpy 0.4.x](http://www.cvxpy.org/en/latest/) +- [cvxpy](http://www.cvxpy.org/en/latest/) + +# Documentation + +This README only shows some examples of this project. + +Full documentation is available online: [https://pythonrobotics.readthedocs.io/](https://pythonrobotics.readthedocs.io/) # How to use -1. Install the required libraries. +1. Install the required libraries. You can use environment.yml with conda command. 2. Clone this repo. @@ -96,19 +119,11 @@ the green point is positioning observation (ex. GPS), and the red line is estima The red ellipse is estimated covariance ellipse with EKF. -## Unscented Kalman Filter localization - -![2](https://github.com/AtsushiSakai/PythonRobotics/raw/master/Localization/unscented_kalman_filter/animation.gif) - -This is a sensor fusion localization with Unscented Kalman Filter(UKF). - -The lines and points are same meaning of the EKF simulation. - Ref: -- [Discriminatively Trained Unscented Kalman Filter for Mobile Robot Localization](https://www.researchgate.net/publication/267963417_Discriminatively_Trained_Unscented_Kalman_Filter_for_Mobile_Robot_Localization) +- [PROBABILISTIC ROBOTICS](http://www.probabilistic-robotics.org/) -## Particle Filter localization +## Particle filter localization ![2](https://github.com/AtsushiSakai/PythonRobotics/raw/master/Localization/particle_filter/animation.gif) @@ -122,11 +137,36 @@ It is assumed that the robot can measure a distance from landmarks (RFID). This measurements are used for PF localization. +Ref: + +- [PROBABILISTIC ROBOTICS](http://www.probabilistic-robotics.org/) + + +## Histogram filter localization + +![3](https://github.com/AtsushiSakai/PythonRobotics/raw/master/Localization/histogram_filter/animation.gif) + +This is a 2D localization example with Histogram filter. + +The red cross is true position, black points are RFID positions. + +The blue grid shows a position probability of histogram filter. + +In this simulation, x,y are unknown, yaw is known. + +The filter integrates speed input and range observations from RFID for localization. + +Initial position is not needed. + +Ref: + +- [PROBABILISTIC ROBOTICS](http://www.probabilistic-robotics.org/) + # Mapping ## Gaussian grid map -This is a 2D gaussian grid mapping example. +This is a 2D Gaussian grid mapping example. ![2](https://github.com/AtsushiSakai/PythonRobotics/raw/master/Mapping/gaussian_grid_map/animation.gif) @@ -136,6 +176,11 @@ This is a 2D ray casting grid mapping example. ![2](https://github.com/AtsushiSakai/PythonRobotics/raw/master/Mapping/raycasting_grid_map/animation.gif) +## k-means object clustering + +This is a 2D object clustering with k-means algorithm. + +![2](https://github.com/AtsushiSakai/PythonRobotics/raw/master/Mapping/kmeans_clustering/animation.gif) # SLAM @@ -154,20 +199,46 @@ Ref: - [Introduction to Mobile Robotics: Iterative Closest Point Algorithm](https://cs.gmu.edu/~kosecka/cs685/cs685-icp.pdf) -## EKF SLAM +## FastSLAM 1.0 -This is a Extended Kalman Filter based SLAM example. +This is a feature based SLAM example using FastSLAM 1.0. -The blue line is ground truth, the black line is dead reckoning, the red line is the estimated trajectory with EKF SLAM. +The blue line is ground truth, the black line is dead reckoning, the red line is the estimated trajectory with FastSLAM. -The green cross are estimated landmarks. +The red points are particles of FastSLAM. + +Black points are landmarks, blue crosses are estimated landmark positions by FastSLAM. + + +![3](https://github.com/AtsushiSakai/PythonRobotics/raw/master/SLAM/FastSLAM1/animation.gif) -![3](https://github.com/AtsushiSakai/PythonRobotics/raw/master/SLAM/EKFSLAM/animation.gif) Ref: - [PROBABILISTIC ROBOTICS](http://www.probabilistic-robotics.org/) +- [SLAM simulations by Tim Bailey](http://www-personal.acfr.usyd.edu.au/tbailey/software/slam_simulations.htm) + + +## Graph based SLAM + +This is a graph based SLAM example. + +The blue line is ground truth. + +The black line is dead reckoning. + +The red line is the estimated trajectory with Graph based SLAM. + +The black stars are landmarks for graph edge generation. + +![3](https://github.com/AtsushiSakai/PythonRobotics/raw/master/SLAM/GraphBasedSLAM/animation.gif) + +Ref: + +- [A Tutorial on Graph-Based SLAM](http://www2.informatik.uni-freiburg.de/~stachnis/pdf/grisetti10titsmag.pdf) + + # Path Planning ## Dynamic Window Approach @@ -197,7 +268,7 @@ This is a 2D grid based shortest path planning with A star algorithm. In the animation, cyan points are searched nodes. -It's heuristic is 2D Euclid distance. +Its heuristic is 2D Euclid distance. ### Potential Field algorithm @@ -211,36 +282,17 @@ Ref: - [Robotic Motion Planning:Potential Functions](https://www.cs.cmu.edu/~motionplanning/lecture/Chap4-Potential-Field_howie.pdf) - -## Model Predictive Trajectory Generator - -This is a path optimization sample on model predictive trajectory generator. - -This algorithm is used for state lattice planner. - -### Path optimization sample - -![PythonRobotics/figure_1.png at master · AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/ModelPredictiveTrajectoryGenerator/kn05animation.gif) - -### Lookup table generation sample - -![PythonRobotics/figure_1.png at master · AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/ModelPredictiveTrajectoryGenerator/lookuptable.png?raw=True) - -Ref: - -- [Optimal rough terrain trajectory generation for wheeled mobile robots](http://journals.sagepub.com/doi/pdf/10.1177/0278364906075328) - - ## State Lattice Planning This script is a path planning code with state lattice planning. This code uses the model predictive trajectory generator to solve boundary problem. +Ref: -### Uniform polar sampling +- [Optimal rough terrain trajectory generation for wheeled mobile robots](http://journals.sagepub.com/doi/pdf/10.1177/0278364906075328) -![PythonRobotics/figure_1.png at master · AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/StateLatticePlanner/UniformPolarSampling.gif) +- [State Space Sampling of Feasible Motions for High-Performance Mobile Robot Navigation in Complex Environments](http://www.frc.ri.cmu.edu/~alonzo/pubs/papers/JFR_08_SS_Sampling.pdf) ### Biased polar sampling @@ -269,38 +321,14 @@ Ref: - [Probabilistic roadmap \- Wikipedia](https://en.wikipedia.org/wiki/Probabilistic_roadmap)    -## Voronoi Road-Map planning - -![VRM](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/VoronoiRoadMap/animation.gif) - -This Voronoi road-map planner uses Dijkstra method for graph search. - -In the animation, blue points are Voronoi points, - -Cyan crosses means searched points with Dijkstra method, - -The red line is the final path of Vornoi Road-Map. - -Ref: - -- [Robotic Motion Planning](https://www.cs.cmu.edu/~motionplanning/lecture/Chap5-RoadMap-Methods_howie.pdf) - ## Rapidly-Exploring Random Trees (RRT) -### Basic RRT - -![PythonRobotics/figure_1.png at master · AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/RRT/animation.gif) - -This script is a simple path planning code with Rapidly-Exploring Random Trees (RRT) - -Black circles are obstacles, green line is a searched tree, red crosses are start and goal positions. - ### RRT\* ![PythonRobotics/figure_1.png at master · AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/RRTstar/animation.gif) -This script is a path planning code with RRT\* +This is a path planning code with RRT\* Black circles are obstacles, green line is a searched tree, red crosses are start and goal positions. @@ -310,92 +338,26 @@ Ref: - [Sampling-based Algorithms for Optimal Motion Planning](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.419.5503&rep=rep1&type=pdf) - -### RRT with dubins path - -![PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/RRTDubins/animation.gif) - -Path planning for a car robot with RRT and dubins path planner. - - -### RRT\* with dubins path - -![AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/RRTStarDubins/animation.gif) - -Path planning for a car robot with RRT\* and dubins path planner. - - ### RRT\* with reeds-sheep path ![Robotics/animation.gif at master · AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/RRTStarReedsShepp/animation.gif)) Path planning for a car robot with RRT\* and reeds sheep path planner. -### Closed Loop RRT\* +### LQR-RRT\* -A vehicle model based path planning with closed loop RRT\*. +This is a path planning simulation with LQR-RRT\*. -![CLRRT](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/ClosedLoopRRTStar/animation.gif) +A double integrator motion model is used for LQR local planner. -In this code, pure-pursuit algorithm is used for steering control, - -PID is used for speed control. +![LQRRRT](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/LQRRRTStar/animation.gif) Ref: -- [Motion Planning in Complex Environments -using Closed-loop Prediction](http://acl.mit.edu/papers/KuwataGNC08.pdf) +- [LQR\-RRT\*: Optimal Sampling\-Based Motion Planning with Automatically Derived Extension Heuristics](http://lis.csail.mit.edu/pubs/perez-icra12.pdf) -- [Real-time Motion Planning with Applications to -Autonomous Urban Driving](http://acl.mit.edu/papers/KuwataTCST09.pdf) +- [MahanFathi/LQR\-RRTstar: LQR\-RRT\* method is used for random motion planning of a simple pendulum in its phase plot](https://github.com/MahanFathi/LQR-RRTstar) -- [[1601.06326] Sampling-based Algorithms for Optimal Motion Planning Using Closed-loop Prediction](https://arxiv.org/abs/1601.06326) - -## Cubic spline planning - -A sample code for cubic path planning. - -This code generates a curvature continuous path based on x-y waypoints with cubic spline. - -Heading angle of each point can be also calculated analytically. - -![PythonRobotics/figure_1.png at master · AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/CubicSpline/Figure_1.png?raw=True) -![PythonRobotics/figure_1.png at master · AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/CubicSpline/Figure_2.png?raw=True) -![PythonRobotics/figure_1.png at master · AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/CubicSpline/Figure_3.png?raw=True) - - -## B-Spline planning - -![B-Spline](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/BSplinePath/Figure_1.png?raw=True) - -This is a path planning with B-Spline curse. - -If you input waypoints, it generates a smooth path with B-Spline curve. - -The final course should be on the first and last waypoints. - -Ref: - -- [B\-spline \- Wikipedia](https://en.wikipedia.org/wiki/B-spline) - -## Bezier path planning - -A sample code of Bezier path planning. - -It is based on 4 control points Beier path. - -![Bezier1](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/BezierPath/Figure_1.png?raw=True) - -If you change the offset distance from start and end point, - -You can get different Beizer course: - -![Bezier2](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/BezierPath/Figure_2.png?raw=True) - - -Ref: - -- [Continuous Curvature Path Generation Based on Bezier Curves for Autonomous Vehicles](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.294.6438&rep=rep1&type=pdf) ## Quintic polynomials planning @@ -409,17 +371,6 @@ Ref: - [Local Path Planning And Motion Control For Agv In Positioning](http://ieeexplore.ieee.org/document/637936/) - -## Dubins path planning - -A sample code for Dubins path planning. - -![dubins](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/DubinsPath/animation.gif?raw=True) - -Ref: - -- [Dubins path - Wikipedia](https://en.wikipedia.org/wiki/Dubins_path) - ## Reeds Shepp planning A sample code with Reeds Shepp path planning. @@ -435,6 +386,13 @@ Ref: - [ghliu/pyReedsShepp: Implementation of Reeds Shepp curve\.](https://github.com/ghliu/pyReedsShepp) +## LQR based path planning + +A sample code using LQR based path planning for double integrator model. + +![RSPlanning](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/LQRPlanner/animation.gif?raw=true) + + ## Optimal Trajectory in a Frenet Frame ![3](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/FrenetOptimalTrajectory/animation.gif) @@ -454,17 +412,16 @@ Ref: # Path tracking -## Pure pursuit tracking +## move to a pose control -Path tracking simulation with pure pursuit steering control and PID speed control. +This is a simulation of moving to a pose control -![2](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathTracking/pure_pursuit/animation.gif) - -The red line is a target course, the green cross means the target point for pure pursuit control, the blue line is the tracking. +![2](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathTracking/move_to_pose/animation.gif) Ref: -- [A Survey of Motion Planning and Control Techniques for Self-driving Urban Vehicles](https://arxiv.org/abs/1604.07446) +- [P. I. Corke, "Robotics, Vision and Control" \| SpringerLink p102](https://link.springer.com/book/10.1007/978-3-642-20144-8) + ## Stanley control @@ -491,17 +448,6 @@ Ref: - [A Survey of Motion Planning and Control Techniques for Self-driving Urban Vehicles](https://arxiv.org/abs/1604.07446) -## Linear–quadratic regulator (LQR) steering control - -Path tracking simulation with LQR steering control and PID speed control. - -![PythonRobotics/figure_1.png at master · AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathTracking/lqr_steer_control/animation.gif) - -Ref: - -- [ApolloAuto/apollo: An open autonomous driving platform](https://github.com/ApolloAuto/apollo) - - ## Linear–quadratic regulator (LQR) speed and steering control Path tracking simulation with LQR speed and steering control. @@ -519,17 +465,33 @@ Path tracking simulation with iterative linear model predictive speed and steeri -This code uses cvxpy as an optimization modeling tool. - -- [Welcome to CVXPY](http://www.cvxpy.org/en/latest/) - Ref: -- [Vehicle Dynamics and Control \| Rajesh Rajamani \| Springer](http://www.springer.com/us/book/9781461414322) +- [notebook](https://github.com/AtsushiSakai/PythonRobotics/blob/master/PathTracking/model_predictive_speed_and_steer_control/notebook.ipynb) -- [MPC Course Material \- MPC Lab @ UC\-Berkeley](http://www.mpc.berkeley.edu/mpc-course-material) - +# Arm Navigation + +## N joint arm to point control + +N joint arm to a point control simulation. + +This is a interactive simulation. + +You can set the goal position of the end effector with left-click on the ploting area. + +![3](https://github.com/AtsushiSakai/PythonRobotics/raw/master/ArmNavigation/n_joint_arm_to_point_control/animation.gif) + +In this simulation N = 10, however, you can change it. + +# Aerial Navigation + +## drone 3d trajectory following + +This is a 3d trajectory following simulation for a quadrotor. + +![3](https://github.com/AtsushiSakai/PythonRobotics/raw/master/AerialNavigation/drone_3d_trajectory_following/animation.gif) + # License @@ -537,14 +499,41 @@ MIT # Contribution -If you have any quesions or problems about this code, feel free to add an issue. - A small PR like bug fix is welcome. -# Author - -Atsushi Sakai ([@Atsushi_twi](https://twitter.com/Atsushi_twi)) +If your PR is merged multiple times, I will add your account to the author list. +# Support + +If you or your company would like to support this project, please consider: + +- [Become a backer or sponsor on Patreon](https://www.patreon.com/myenigma) + +- [One-time donation via PayPal](https://www.paypal.me/myenigmapay/) + +You can add your name or your company logo in README if you are a patron. + +E-mail consultant is also available. + +   + +Your comment using [![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/AtsushiSakai) is also welcome. + +This is a list: [Users comments](https://github.com/AtsushiSakai/PythonRobotics/blob/master/users_comments.md) + +# Authors + +- [Atsushi Sakai](https://github.com/AtsushiSakai/) ([@Atsushi_twi](https://twitter.com/Atsushi_twi)) + +- [Daniel Ingram](https://github.com/daniel-s-ingram) + +- [Joe Dinius](https://github.com/jwdinius) + +- [Karan Chawla](https://github.com/karanchawla) + +- [Antonin RAFFIN](https://github.com/araffin) + +- [Alexis Paques](https://github.com/AlexisTM) diff --git a/RoboticArm/two_joint_arm.py b/RoboticArm/two_joint_arm.py deleted file mode 100644 index 4e345a49..00000000 --- a/RoboticArm/two_joint_arm.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Inverse kinematics of a two-joint arm -Left-click the plot to set the goal position of the end effector - -Author: Daniel Ingram (daniel-s-ingram) -""" -from __future__ import print_function, division - -import matplotlib.pyplot as plt -import numpy as np - -from math import sin, cos, atan2, acos, pi - -Kp = 15 -dt = 0.01 - -#Link lengths -l1 = l2 = 1 - -shoulder = np.array([0, 0]) - -#Set initial goal position to the initial end-effector position -x = 2 -y = 0 - -plt.ion() - -def two_joint_arm(): - """ - Computes the inverse kinematics for a planar 2DOF arm - """ - theta1 = theta2 = 0 - while True: - try: - theta2_goal = acos((x**2 + y**2 - l1**2 -l2**2)/(2*l1*l2)) - theta1_goal = atan2(y, x) - atan2(l2*sin(theta2_goal), (l1 + l2*cos(theta2_goal))) - - if theta1_goal < 0: - theta2_goal = -theta2_goal - theta1_goal = atan2(y, x) - atan2(l2*sin(theta2_goal), (l1 + l2*cos(theta2_goal))) - - theta1 = theta1 + Kp*ang_diff(theta1_goal, theta1)*dt - theta2 = theta2 + Kp*ang_diff(theta2_goal, theta2)*dt - except ValueError as e: - print("Unreachable goal") - - plot_arm(theta1, theta2, x, y) - -def plot_arm(theta1, theta2, x, y): - elbow = shoulder + np.array([l1*cos(theta1), l1*sin(theta1)]) - wrist = elbow + np.array([l2*cos(theta1+theta2), l2*sin(theta1+theta2)]) - - plt.cla() - - plt.plot([shoulder[0], elbow[0]], [shoulder[1], elbow[1]], 'k-') - plt.plot([elbow[0], wrist[0]], [elbow[1], wrist[1]], 'k-') - - plt.plot(shoulder[0], shoulder[1], 'ro') - plt.plot(elbow[0], elbow[1], 'ro') - plt.plot(wrist[0], wrist[1], 'ro') - - plt.plot([wrist[0], x], [wrist[1], y], 'g--') - plt.plot(x, y, 'g*') - - plt.xlim(-2, 2) - plt.ylim(-2, 2) - - plt.show() - plt.pause(dt) - -def ang_diff(theta1, theta2): - #Returns the difference between two angles in the range -pi to +pi - return (theta1-theta2+pi)%(2*pi)-pi - -def click(event): - global x, y - x = event.xdata - y = event.ydata - -if __name__ == "__main__": - fig = plt.figure() - fig.canvas.mpl_connect("button_press_event", click) - two_joint_arm() diff --git a/SLAM/EKFSLAM/ekf_slam.py b/SLAM/EKFSLAM/ekf_slam.py index 776fb017..c8b5ae03 100644 --- a/SLAM/EKFSLAM/ekf_slam.py +++ b/SLAM/EKFSLAM/ekf_slam.py @@ -203,13 +203,7 @@ def jacobH(q, delta, x, i): def pi_2_pi(angle): - while(angle > math.pi): - angle = angle - 2.0 * math.pi - - while(angle < -math.pi): - angle = angle + 2.0 * math.pi - - return angle + return (angle + math.pi) % (2*math.pi) - math.pi def main(): diff --git a/SLAM/FastSLAM1/animation.gif b/SLAM/FastSLAM1/animation.gif new file mode 100644 index 00000000..f4dbc721 Binary files /dev/null and b/SLAM/FastSLAM1/animation.gif differ diff --git a/SLAM/FastSLAM/fast_slam.py b/SLAM/FastSLAM1/fast_slam1.py similarity index 61% rename from SLAM/FastSLAM/fast_slam.py rename to SLAM/FastSLAM1/fast_slam1.py index 0730303b..eb2f03fa 100644 --- a/SLAM/FastSLAM/fast_slam.py +++ b/SLAM/FastSLAM1/fast_slam1.py @@ -1,6 +1,6 @@ """ -Fast SLAM example +FastSLAM 1.0 example author: Atsushi Sakai (@Atsushi_twi) @@ -11,12 +11,14 @@ import math import matplotlib.pyplot as plt -# EKF state covariance -Cx = np.diag([1.0, 1.0, math.radians(30.0)])**2 +# Fast SLAM covariance +Q = np.diag([3.0, math.radians(10.0)])**2 +R = np.diag([1.0, math.radians(20.0)])**2 # Simulation parameter -Qsim = np.diag([0.0, math.radians(0.0)])**2 -Rsim = np.diag([1.0, math.radians(10.0)])**2 +Qsim = np.diag([0.3, math.radians(2.0)])**2 +Rsim = np.diag([0.5, math.radians(10.0)])**2 +OFFSET_YAWRATE_NOISE = 0.01 DT = 0.1 # time tick [s] SIM_TIME = 50.0 # simulation time [s] @@ -24,10 +26,8 @@ MAX_RANGE = 20.0 # maximum observation range M_DIST_TH = 2.0 # Threshold of Mahalanobis distance for data association. STATE_SIZE = 3 # State size [x,y,yaw] LM_SIZE = 2 # LM srate size [x,y] - N_PARTICLE = 100 # number of particle - -NTH = N_PARTICLE / 2.0 # Number of particle for re-sampling +NTH = N_PARTICLE / 1.5 # Number of particle for re-sampling show_animation = True @@ -39,22 +39,35 @@ class Particle: self.x = 0.0 self.y = 0.0 self.yaw = 0.0 - self.lm = np.zeros((N_LM, 2)) - self.lmP = [np.zeros((2, 2))] * N_LM + # landmark x-y positions + self.lm = np.matrix(np.zeros((N_LM, LM_SIZE))) + # landmark position covariance + self.lmP = np.matrix(np.zeros((N_LM * LM_SIZE, LM_SIZE))) + + +def fast_slam1(particles, u, z): + + particles = predict_particles(particles, u) + + particles = update_with_observation(particles, z) + + particles = resampling(particles) + + return particles def normalize_weight(particles): sumw = sum([p.w for p in particles]) - # print(sumw) - # if sumw <= 0.0000001: - # for i in range(N_PARTICLE): - # particles[i].w = 1.0 / N_PARTICLE - # return particles + try: + for i in range(N_PARTICLE): + particles[i].w /= sumw + except ZeroDivisionError: + for i in range(N_PARTICLE): + particles[i].w = 1.0 / N_PARTICLE - for i in range(N_PARTICLE): - particles[i].w = particles[i].w / sumw + return particles return particles @@ -69,7 +82,6 @@ def calc_final_state(particles): xEst[0, 0] += particles[i].w * particles[i].x xEst[1, 0] += particles[i].w * particles[i].y xEst[2, 0] += particles[i].w * particles[i].yaw - # print(particles[i].x, particles[i].y, particles[i].yaw, particles[i].w) xEst[2, 0] = pi_2_pi(xEst[2, 0]) # print(xEst) @@ -84,7 +96,7 @@ def predict_particles(particles, u): px[0, 0] = particles[i].x px[1, 0] = particles[i].y px[2, 0] = particles[i].yaw - ud = u + (np.matrix(np.random.randn(1, 2)) * Rsim).T # add noise + ud = u + (np.matrix(np.random.randn(1, 2)) * R).T # add noise px = motion_model(px, ud) particles[i].x = px[0, 0] particles[i].y = px[1, 0] @@ -93,110 +105,102 @@ def predict_particles(particles, u): return particles -def add_new_lm(particle, z): +def add_new_lm(particle, z, Q): r = z[0, 0] b = z[0, 1] lm_id = int(z[0, 2]) - s = math.sin(particle.yaw + b) - c = math.cos(particle.yaw + b) + s = math.sin(pi_2_pi(particle.yaw + b)) + c = math.cos(pi_2_pi(particle.yaw + b)) particle.lm[lm_id, 0] = particle.x + r * c particle.lm[lm_id, 1] = particle.y + r * s - # print(particle.lm) - # print(lm_id) # covariance Gz = np.matrix([[c, -r * s], [s, r * c]]) - particle.lmP[lm_id] = Gz * Cx[0:2, 0:2] * Gz.T + particle.lmP[2 * lm_id:2 * lm_id + 2] = Gz * Q * Gz.T return particle -def compute_jacobians(particle, xf, Pf, R): - dx = xf[0] - particle.x - dy = xf[1] - particle.y +def compute_jacobians(particle, xf, Pf, Q): + dx = xf[0, 0] - particle.x + dy = xf[1, 0] - particle.y d2 = dx**2 + dy**2 d = math.sqrt(d2) - zp = np.matrix([[d, pi_2_pi(math.atan2(dy, dx) - particle.yaw)]]) + zp = np.matrix([[d, pi_2_pi(math.atan2(dy, dx) - particle.yaw)]]).T Hv = np.matrix([[-dx / d, -dy / d, 0.0], [dy / d2, -dx / d2, -1.0]]) - Hf = np.matrix([[dx / d, -dy / d], + Hf = np.matrix([[dx / d, dy / d], [-dy / d2, dx / d2]]) - Sf = Hf * Pf * Hf.T + R + Sf = Hf * Pf * Hf.T + Q return zp, Hv, Hf, Sf -def KF_cholesky_update(xf, Pf, v, R, Hf): +def update_KF_with_cholesky(xf, Pf, v, Q, Hf): PHt = Pf * Hf.T - S = Hf * PHt + R + S = Hf * PHt + Q S = (S + S.T) * 0.5 SChol = np.linalg.cholesky(S).T - SCholInv = np.linalg.inv(SChol) W1 = PHt * SCholInv W = W1 * SCholInv.T - x = xf + (W * v.T).T + x = xf + W * v P = Pf - W1 * W1.T return x, P -def feature_update(particle, z, R): +def update_landmark(particle, z, Q): lm_id = int(z[0, 2]) - xf = particle.lm[lm_id, :] - Pf = particle.lmP[lm_id] - # print(xf) - # print(particle.lm) + xf = np.matrix(particle.lm[lm_id, :]).T + Pf = np.matrix(particle.lmP[2 * lm_id:2 * lm_id + 2, :]) - zp, Hv, Hf, Sf = compute_jacobians(particle, xf, Pf, R) + zp, Hv, Hf, Sf = compute_jacobians(particle, xf, Pf, Q) - v = z[0, 0:2] - zp - v[0, 1] = pi_2_pi(v[0, 1]) - # print(v) + dz = z[0, 0: 2].T - zp + dz[1, 0] = pi_2_pi(dz[1, 0]) - xf, Pf = KF_cholesky_update(xf, Pf, v, R, Hf) + xf, Pf = update_KF_with_cholesky(xf, Pf, dz, Q, Hf) - particle.lm[lm_id, :] = xf - particle.lmP[lm_id] = Pf - - # print(xf) - # print(particle.lm) - # print(Pf) - # input() + particle.lm[lm_id, :] = xf.T + particle.lmP[2 * lm_id:2 * lm_id + 2, :] = Pf return particle -def compute_weight(particle, z, R): +def compute_weight(particle, z, Q): lm_id = int(z[0, 2]) - xf = particle.lm[lm_id, :] - Pf = particle.lmP[lm_id] - zp, Hv, Hf, Sf = compute_jacobians(particle, xf, Pf, R) + xf = np.matrix(particle.lm[lm_id, :]).T + Pf = np.matrix(particle.lmP[2 * lm_id:2 * lm_id + 2]) + zp, Hv, Hf, Sf = compute_jacobians(particle, xf, Pf, Q) - dx = z[0, 0:2] - zp - dx[0, 1] = pi_2_pi(dx[0, 1]) - dx = dx.T + dx = z[0, 0: 2].T - zp + dx[1, 0] = pi_2_pi(dx[1, 0]) - S = particle.lmP[lm_id] + S = particle.lmP[2 * lm_id:2 * lm_id + 2] + try: + invS = np.linalg.inv(S) + except np.linalg.linalg.LinAlgError: + print("singuler") + return 1.0 - num = math.exp(-0.5 * dx.T * np.linalg.inv(S) * dx) + num = math.exp(-0.5 * dx.T * invS * dx) den = 2.0 * math.pi * math.sqrt(np.linalg.det(S)) w = num / den - print(w) return w @@ -209,15 +213,13 @@ def update_with_observation(particles, z): for ip in range(N_PARTICLE): # new landmark - if abs(particles[ip].lm[lmid, 0]) <= 0.1: - particles[ip] = add_new_lm(particles[ip], z[iz, :]) + if abs(particles[ip].lm[lmid, 0]) <= 0.01: + particles[ip] = add_new_lm(particles[ip], z[iz, :], Q) # known landmark else: - # w = p(z_k | x_k) - w = compute_weight(particles[ip], z[iz, :], Cx[0:2, 0:2]) - particles[ip].w = particles[ip].w * w - particles[ip] = feature_update( - particles[ip], z[iz, :], Cx[0:2, 0:2]) + w = compute_weight(particles[ip], z[iz, :], Q) + particles[ip].w *= w + particles[ip] = update_landmark(particles[ip], z[iz, :], Q) return particles @@ -236,10 +238,9 @@ def resampling(particles): pw = np.matrix(pw) Neff = 1.0 / (pw * pw.T)[0, 0] # Effective particle number - print(Neff) + # print(Neff) if Neff < NTH: # resampling - print("resamping") wcum = np.cumsum(pw) base = np.cumsum(pw * 0.0 + 1 / N_PARTICLE) - 1 / N_PARTICLE resampleid = base + np.random.rand(base.shape[1]) / N_PARTICLE @@ -251,41 +252,29 @@ def resampling(particles): ind += 1 inds.append(ind) - # print(inds) - # print(pw) - tparticles = particles[:] for i in range(len(inds)): - particles[i] = tparticles[inds[i]] + particles[i].x = tparticles[inds[i]].x + particles[i].y = tparticles[inds[i]].y + particles[i].yaw = tparticles[inds[i]].yaw + particles[i].lm = tparticles[inds[i]].lm[:, :] + particles[i].lmP = tparticles[inds[i]].lmP[:, :] particles[i].w = 1.0 / N_PARTICLE - particles = normalize_weight(particles) - # input() - return particles -def fast_slam(particles, PEst, u, z): +def calc_input(time): - # Predict - particles = predict_particles(particles, u) + if time <= 3.0: + v = 0.0 + yawrate = 0.0 + else: + v = 1.0 # [m/s] + yawrate = 0.1 # [rad/s] - # Observation - particles = update_with_observation(particles, z) - - particles = normalize_weight(particles) - - particles = resampling(particles) - - xEst = calc_final_state(particles) - - return xEst, PEst - - -def calc_input(): - v = 1.0 # [m/s] - yawrate = 0.1 # [rad/s] u = np.matrix([v, yawrate]).T + return u @@ -301,16 +290,16 @@ def observation(xTrue, xd, u, RFID): dx = RFID[i, 0] - xTrue[0, 0] dy = RFID[i, 1] - xTrue[1, 0] d = math.sqrt(dx**2 + dy**2) - angle = pi_2_pi(math.atan2(dy, dx)) + angle = pi_2_pi(math.atan2(dy, dx) - xTrue[2, 0]) if d <= MAX_RANGE: dn = d + np.random.randn() * Qsim[0, 0] # add noise anglen = angle + np.random.randn() * Qsim[1, 1] # add noise - zi = np.matrix([dn, anglen, i]) + zi = np.matrix([dn, pi_2_pi(anglen), i]) z = np.vstack((z, zi)) # add noise to input ud1 = u[0, 0] + np.random.randn() * Rsim[0, 0] - ud2 = u[1, 0] + np.random.randn() * Rsim[1, 1] + ud2 = u[1, 0] + np.random.randn() * Rsim[1, 1] + OFFSET_YAWRATE_NOISE ud = np.matrix([ud1, ud2]).T xd = motion_model(xd, ud) @@ -335,36 +324,8 @@ def motion_model(x, u): return x -def calc_n_LM(x): - n = int((len(x) - STATE_SIZE) / LM_SIZE) - return n - - -def calc_LM_Pos(x, z): - - zp = np.zeros((2, 1)) - - zp[0, 0] = x[0, 0] + z[0, 0] * math.cos(x[2, 0] + z[0, 1]) - zp[1, 0] = x[1, 0] + z[0, 0] * math.sin(x[2, 0] + z[0, 1]) - - return zp - - -def get_LM_Pos_from_state(x, ind): - - lm = x[STATE_SIZE + LM_SIZE * ind: STATE_SIZE + LM_SIZE * (ind + 1), :] - - return lm - - def pi_2_pi(angle): - while(angle > math.pi): - angle = angle - 2.0 * math.pi - - while(angle < -math.pi): - angle = angle + 2.0 * math.pi - - return angle + return (angle + math.pi) % (2*math.pi) - math.pi def main(): @@ -375,14 +336,18 @@ def main(): # RFID positions [x, y] RFID = np.array([[10.0, -2.0], [15.0, 10.0], + [15.0, 15.0], + [10.0, 20.0], [3.0, 15.0], - [-5.0, 20.0]]) + [-5.0, 20.0], + [-5.0, 5.0], + [-10.0, 15.0] + ]) N_LM = RFID.shape[0] # State Vector [x y yaw v]' xEst = np.matrix(np.zeros((STATE_SIZE, 1))) xTrue = np.matrix(np.zeros((STATE_SIZE, 1))) - PEst = np.eye(STATE_SIZE) xDR = np.matrix(np.zeros((STATE_SIZE, 1))) # Dead reckoning @@ -395,13 +360,15 @@ def main(): while SIM_TIME >= time: time += DT - u = calc_input() + u = calc_input(time) xTrue, z, xDR, ud = observation(xTrue, xDR, u, RFID) - xEst, PEst = fast_slam(particles, PEst, ud, z) + particles = fast_slam1(particles, ud, z) - x_state = xEst[0:STATE_SIZE] + xEst = calc_final_state(particles) + + x_state = xEst[0: STATE_SIZE] # store data history hxEst = np.hstack((hxEst, x_state)) @@ -410,20 +377,11 @@ def main(): if show_animation: plt.cla() - plt.plot(RFID[:, 0], RFID[:, 1], "*k") for i in range(N_PARTICLE): plt.plot(particles[i].x, particles[i].y, ".r") - - # for ii in range(N_LM): - # plt.plot(particles[i].lm[ii, 0], - # particles[i].lm[ii, 1], "xb") - - # plot landmark - for i in range(calc_n_LM(xEst)): - plt.plot(xEst[STATE_SIZE + i * 2], - xEst[STATE_SIZE + i * 2 + 1], "xg") + plt.plot(particles[i].lm[:, 0], particles[i].lm[:, 1], "xb") plt.plot(np.array(hxTrue[0, :]).flatten(), np.array(hxTrue[1, :]).flatten(), "-b") diff --git a/SLAM/FastSLAM2/animation.gif b/SLAM/FastSLAM2/animation.gif new file mode 100644 index 00000000..4fa070b5 Binary files /dev/null and b/SLAM/FastSLAM2/animation.gif differ diff --git a/SLAM/FastSLAM2/fast_slam2.py b/SLAM/FastSLAM2/fast_slam2.py new file mode 100644 index 00000000..76fb2928 --- /dev/null +++ b/SLAM/FastSLAM2/fast_slam2.py @@ -0,0 +1,434 @@ +""" + +FastSLAM 2.0 example + +author: Atsushi Sakai (@Atsushi_twi) + +""" + +import numpy as np +import math +import matplotlib.pyplot as plt + + +# Fast SLAM covariance +Q = np.diag([3.0, math.radians(10.0)])**2 +R = np.diag([1.0, math.radians(20.0)])**2 + +# Simulation parameter +Qsim = np.diag([0.3, math.radians(2.0)])**2 +Rsim = np.diag([0.5, math.radians(10.0)])**2 +OFFSET_YAWRATE_NOISE = 0.01 + +DT = 0.1 # time tick [s] +SIM_TIME = 50.0 # simulation time [s] +MAX_RANGE = 20.0 # maximum observation range +M_DIST_TH = 2.0 # Threshold of Mahalanobis distance for data association. +STATE_SIZE = 3 # State size [x,y,yaw] +LM_SIZE = 2 # LM srate size [x,y] +N_PARTICLE = 100 # number of particle +NTH = N_PARTICLE / 1.0 # Number of particle for re-sampling + +show_animation = True + + +class Particle: + + def __init__(self, N_LM): + self.w = 1.0 / N_PARTICLE + self.x = 0.0 + self.y = 0.0 + self.yaw = 0.0 + self.P = np.eye(3) + # landmark x-y positions + self.lm = np.matrix(np.zeros((N_LM, LM_SIZE))) + # landmark position covariance + self.lmP = np.matrix(np.zeros((N_LM * LM_SIZE, LM_SIZE))) + + +def fast_slam2(particles, u, z): + + particles = predict_particles(particles, u) + + particles = update_with_observation(particles, z) + + particles = resampling(particles) + + return particles + + +def normalize_weight(particles): + + sumw = sum([p.w for p in particles]) + + try: + for i in range(N_PARTICLE): + particles[i].w /= sumw + except ZeroDivisionError: + for i in range(N_PARTICLE): + particles[i].w = 1.0 / N_PARTICLE + + return particles + + return particles + + +def calc_final_state(particles): + + xEst = np.zeros((STATE_SIZE, 1)) + + particles = normalize_weight(particles) + + for i in range(N_PARTICLE): + xEst[0, 0] += particles[i].w * particles[i].x + xEst[1, 0] += particles[i].w * particles[i].y + xEst[2, 0] += particles[i].w * particles[i].yaw + + xEst[2, 0] = pi_2_pi(xEst[2, 0]) + # print(xEst) + + return xEst + + +def predict_particles(particles, u): + + for i in range(N_PARTICLE): + px = np.zeros((STATE_SIZE, 1)) + px[0, 0] = particles[i].x + px[1, 0] = particles[i].y + px[2, 0] = particles[i].yaw + ud = u + (np.matrix(np.random.randn(1, 2)) * R).T # add noise + px = motion_model(px, ud) + particles[i].x = px[0, 0] + particles[i].y = px[1, 0] + particles[i].yaw = px[2, 0] + + return particles + + +def add_new_lm(particle, z, Q): + + r = z[0, 0] + b = z[0, 1] + lm_id = int(z[0, 2]) + + s = math.sin(pi_2_pi(particle.yaw + b)) + c = math.cos(pi_2_pi(particle.yaw + b)) + + particle.lm[lm_id, 0] = particle.x + r * c + particle.lm[lm_id, 1] = particle.y + r * s + + # covariance + Gz = np.matrix([[c, -r * s], + [s, r * c]]) + + particle.lmP[2 * lm_id:2 * lm_id + 2] = Gz * Q * Gz.T + + return particle + + +def compute_jacobians(particle, xf, Pf, Q): + dx = xf[0, 0] - particle.x + dy = xf[1, 0] - particle.y + d2 = dx**2 + dy**2 + d = math.sqrt(d2) + + zp = np.matrix([[d, pi_2_pi(math.atan2(dy, dx) - particle.yaw)]]).T + + Hv = np.matrix([[-dx / d, -dy / d, 0.0], + [dy / d2, -dx / d2, -1.0]]) + + Hf = np.matrix([[dx / d, dy / d], + [-dy / d2, dx / d2]]) + + Sf = Hf * Pf * Hf.T + Q + + return zp, Hv, Hf, Sf + + +def update_KF_with_cholesky(xf, Pf, v, Q, Hf): + PHt = Pf * Hf.T + S = Hf * PHt + Q + + S = (S + S.T) * 0.5 + SChol = np.linalg.cholesky(S).T + SCholInv = np.linalg.inv(SChol) + W1 = PHt * SCholInv + W = W1 * SCholInv.T + + x = xf + W * v + P = Pf - W1 * W1.T + + return x, P + + +def update_landmark(particle, z, Q): + + lm_id = int(z[0, 2]) + xf = np.matrix(particle.lm[lm_id, :]).T + Pf = np.matrix(particle.lmP[2 * lm_id:2 * lm_id + 2, :]) + + zp, Hv, Hf, Sf = compute_jacobians(particle, xf, Pf, Q) + + dz = z[0, 0: 2].T - zp + dz[1, 0] = pi_2_pi(dz[1, 0]) + + xf, Pf = update_KF_with_cholesky(xf, Pf, dz, Q, Hf) + + particle.lm[lm_id, :] = xf.T + particle.lmP[2 * lm_id:2 * lm_id + 2, :] = Pf + + return particle + + +def compute_weight(particle, z, Q): + + lm_id = int(z[0, 2]) + xf = np.matrix(particle.lm[lm_id, :]).T + Pf = np.matrix(particle.lmP[2 * lm_id:2 * lm_id + 2]) + zp, Hv, Hf, Sf = compute_jacobians(particle, xf, Pf, Q) + + dz = z[0, 0: 2].T - zp + dz[1, 0] = pi_2_pi(dz[1, 0]) + + S = particle.lmP[2 * lm_id:2 * lm_id + 2] + try: + invS = np.linalg.inv(S) + except np.linalg.linalg.LinAlgError: + print("singuler") + return 1.0 + + num = math.exp(-0.5 * dz.T * invS * dz) + den = 2.0 * math.pi * math.sqrt(np.linalg.det(S)) + + w = num / den + + return w + + +def proposal_sampling(particle, z, Q): + + lm_id = int(z[0, 2]) + xf = np.matrix(particle.lm[lm_id, :]).T + Pf = np.matrix(particle.lmP[2 * lm_id:2 * lm_id + 2]) + # State + x = np.matrix([[particle.x, particle.y, particle.yaw]]).T + P = particle.P + zp, Hv, Hf, Sf = compute_jacobians(particle, xf, Pf, Q) + + Sfi = np.linalg.inv(Sf) + dz = z[0, 0: 2].T - zp + dz[1, 0] = pi_2_pi(dz[1, 0]) + + Pi = np.linalg.inv(P) + + particle.P = np.linalg.inv(Hv.T * Sfi * Hv + Pi) # proposal covariance + x += particle.P * Hv.T * Sfi * dz # proposal mean + + particle.x = x[0, 0] + particle.y = x[1, 0] + particle.yaw = x[2, 0] + + return particle + + +def update_with_observation(particles, z): + + for iz in range(len(z[:, 0])): + + lmid = int(z[iz, 2]) + + for ip in range(N_PARTICLE): + # new landmark + if abs(particles[ip].lm[lmid, 0]) <= 0.01: + particles[ip] = add_new_lm(particles[ip], z[iz, :], Q) + # known landmark + else: + w = compute_weight(particles[ip], z[iz, :], Q) + particles[ip].w *= w + + particles[ip] = update_landmark(particles[ip], z[iz, :], Q) + particles[ip] = proposal_sampling(particles[ip], z[iz, :], Q) + + return particles + + +def resampling(particles): + """ + low variance re-sampling + """ + + particles = normalize_weight(particles) + + pw = [] + for i in range(N_PARTICLE): + pw.append(particles[i].w) + + pw = np.matrix(pw) + + Neff = 1.0 / (pw * pw.T)[0, 0] # Effective particle number + # print(Neff) + + if Neff < NTH: # resampling + wcum = np.cumsum(pw) + base = np.cumsum(pw * 0.0 + 1 / N_PARTICLE) - 1 / N_PARTICLE + resampleid = base + np.random.rand(base.shape[1]) / N_PARTICLE + + inds = [] + ind = 0 + for ip in range(N_PARTICLE): + while ((ind < wcum.shape[1] - 1) and (resampleid[0, ip] > wcum[0, ind])): + ind += 1 + inds.append(ind) + + tparticles = particles[:] + for i in range(len(inds)): + particles[i].x = tparticles[inds[i]].x + particles[i].y = tparticles[inds[i]].y + particles[i].yaw = tparticles[inds[i]].yaw + particles[i].lm = tparticles[inds[i]].lm[:, :] + particles[i].lmP = tparticles[inds[i]].lmP[:, :] + particles[i].w = 1.0 / N_PARTICLE + + return particles + + +def calc_input(time): + + if time <= 3.0: + v = 0.0 + yawrate = 0.0 + else: + v = 1.0 # [m/s] + yawrate = 0.1 # [rad/s] + + u = np.matrix([v, yawrate]).T + + return u + + +def observation(xTrue, xd, u, RFID): + + xTrue = motion_model(xTrue, u) + + # add noise to gps x-y + z = np.matrix(np.zeros((0, 3))) + + for i in range(len(RFID[:, 0])): + + dx = RFID[i, 0] - xTrue[0, 0] + dy = RFID[i, 1] - xTrue[1, 0] + d = math.sqrt(dx**2 + dy**2) + angle = math.atan2(dy, dx) - xTrue[2, 0] + if d <= MAX_RANGE: + dn = d + np.random.randn() * Qsim[0, 0] # add noise + anglen = angle + np.random.randn() * Qsim[1, 1] # add noise + zi = np.matrix([dn, pi_2_pi(anglen), i]) + z = np.vstack((z, zi)) + + # add noise to input + ud1 = u[0, 0] + np.random.randn() * Rsim[0, 0] + ud2 = u[1, 0] + np.random.randn() * Rsim[1, 1] + OFFSET_YAWRATE_NOISE + ud = np.matrix([ud1, ud2]).T + + xd = motion_model(xd, ud) + + return xTrue, z, xd, ud + + +def motion_model(x, u): + + F = np.matrix([[1.0, 0, 0], + [0, 1.0, 0], + [0, 0, 1.0]]) + + B = np.matrix([[DT * math.cos(x[2, 0]), 0], + [DT * math.sin(x[2, 0]), 0], + [0.0, DT]]) + + x = F * x + B * u + + x[2, 0] = pi_2_pi(x[2, 0]) + + return x + + +def pi_2_pi(angle): + return (angle + math.pi) % (2*math.pi) - math.pi + + +def main(): + print(__file__ + " start!!") + + time = 0.0 + + # RFID positions [x, y] + RFID = np.array([[10.0, -2.0], + [15.0, 10.0], + [15.0, 15.0], + [10.0, 20.0], + [3.0, 15.0], + [-5.0, 20.0], + [-5.0, 5.0], + [-10.0, 15.0] + ]) + N_LM = RFID.shape[0] + + # State Vector [x y yaw v]' + xEst = np.matrix(np.zeros((STATE_SIZE, 1))) + xTrue = np.matrix(np.zeros((STATE_SIZE, 1))) + + xDR = np.matrix(np.zeros((STATE_SIZE, 1))) # Dead reckoning + + # history + hxEst = xEst + hxTrue = xTrue + hxDR = xTrue + + particles = [Particle(N_LM) for i in range(N_PARTICLE)] + + while SIM_TIME >= time: + time += DT + u = calc_input(time) + + xTrue, z, xDR, ud = observation(xTrue, xDR, u, RFID) + + particles = fast_slam2(particles, ud, z) + + xEst = calc_final_state(particles) + + x_state = xEst[0: STATE_SIZE] + + # store data history + hxEst = np.hstack((hxEst, x_state)) + hxDR = np.hstack((hxDR, xDR)) + hxTrue = np.hstack((hxTrue, xTrue)) + + if show_animation: + plt.cla() + plt.plot(RFID[:, 0], RFID[:, 1], "*k") + + for iz in range(len(z[:, 0])): + lmid = int(z[iz, 2]) + plt.plot([xEst[0, 0], RFID[lmid, 0]], [ + xEst[1, 0], RFID[lmid, 1]], "-k") + + for i in range(N_PARTICLE): + plt.plot(particles[i].x, particles[i].y, ".r") + plt.plot(particles[i].lm[:, 0], particles[i].lm[:, 1], "xb") + + plt.plot(np.array(hxTrue[0, :]).flatten(), + np.array(hxTrue[1, :]).flatten(), "-b") + plt.plot(np.array(hxDR[0, :]).flatten(), + np.array(hxDR[1, :]).flatten(), "-k") + plt.plot(np.array(hxEst[0, :]).flatten(), + np.array(hxEst[1, :]).flatten(), "-r") + + plt.plot(xEst[0], xEst[1], "xk") + plt.axis("equal") + plt.grid(True) + plt.pause(0.001) + + +if __name__ == '__main__': + main() diff --git a/SLAM/GraphBasedSLAM/animation.gif b/SLAM/GraphBasedSLAM/animation.gif new file mode 100644 index 00000000..eb866556 Binary files /dev/null and b/SLAM/GraphBasedSLAM/animation.gif differ diff --git a/SLAM/GraphBasedSLAM/graph_based_slam.py b/SLAM/GraphBasedSLAM/graph_based_slam.py new file mode 100644 index 00000000..eb64076d --- /dev/null +++ b/SLAM/GraphBasedSLAM/graph_based_slam.py @@ -0,0 +1,315 @@ +""" + +Graph based SLAM example + +author: Atsushi Sakai (@Atsushi_twi) + +Ref + +[A Tutorial on Graph-Based SLAM](http://www2.informatik.uni-freiburg.de/~stachnis/pdf/grisetti10titsmag.pdf) + +""" + +import numpy as np +import math +import copy +import itertools +import matplotlib.pyplot as plt + + +# Simulation parameter +Qsim = np.diag([0.2, math.radians(1.0)])**2 +Rsim = np.diag([0.1, math.radians(10.0)])**2 + +DT = 2.0 # time tick [s] +SIM_TIME = 100.0 # simulation time [s] +MAX_RANGE = 30.0 # maximum observation range +STATE_SIZE = 3 # State size [x,y,yaw] + +# Covariance parameter of Graph Based SLAM +C_SIGMA1 = 0.1 +C_SIGMA2 = 0.1 +C_SIGMA3 = math.radians(1.0) + +MAX_ITR = 20 # Maximum iteration + +show_graph_dtime = 20.0 # [s] +show_animation = True + + +class Edge(): + + def __init__(self): + self.e = np.zeros((3, 1)) + self.omega = np.zeros((3, 3)) # information matrix + self.d1 = 0.0 + self.d2 = 0.0 + self.yaw1 = 0.0 + self.yaw2 = 0.0 + self.angle1 = 0.0 + self.angle2 = 0.0 + self.id1 = 0 + self.id2 = 0 + + +def cal_observation_sigma(d): + + sigma = np.zeros((3, 3)) + sigma[0, 0] = C_SIGMA1**2 + sigma[1, 1] = C_SIGMA2**2 + sigma[2, 2] = C_SIGMA3**2 + + return sigma + + +def calc_rotational_matrix(angle): + + Rt = np.matrix([[math.cos(angle), -math.sin(angle), 0], + [math.sin(angle), math.cos(angle), 0], + [0, 0, 1.0]]) + return Rt + + +def calc_edge(x1, y1, yaw1, x2, y2, yaw2, d1, + angle1, phi1, d2, angle2, phi2, t1, t2): + edge = Edge() + + tangle1 = pi_2_pi(yaw1 + angle1) + tangle2 = pi_2_pi(yaw2 + angle2) + tmp1 = d1 * math.cos(tangle1) + tmp2 = d2 * math.cos(tangle2) + tmp3 = d1 * math.sin(tangle1) + tmp4 = d2 * math.sin(tangle2) + + edge.e[0, 0] = x2 - x1 - tmp1 + tmp2 + edge.e[1, 0] = y2 - y1 - tmp3 + tmp4 + hyaw = phi1 - phi2 + angle1 - angle2 + edge.e[2, 0] = pi_2_pi(yaw2 - yaw1 - hyaw) + + Rt1 = calc_rotational_matrix(tangle1) + Rt2 = calc_rotational_matrix(tangle2) + + sig1 = cal_observation_sigma(d1) + sig2 = cal_observation_sigma(d2) + + edge.omega = np.linalg.inv(Rt1 * sig1 * Rt1.T + Rt2 * sig2 * Rt2.T) + + edge.d1, edge.d2 = d1, d2 + edge.yaw1, edge.yaw2 = yaw1, yaw2 + edge.angle1, edge.angle2 = angle1, angle2 + edge.id1, edge.id2 = t1, t2 + + return edge + + +def calc_edges(xlist, zlist): + + edges = [] + cost = 0.0 + zids = list(itertools.combinations(range(len(zlist)), 2)) + + for (t1, t2) in zids: + x1, y1, yaw1 = xlist[0, t1], xlist[1, t1], xlist[2, t1] + x2, y2, yaw2 = xlist[0, t2], xlist[1, t2], xlist[2, t2] + + if zlist[t1] is None or zlist[t2] is None: + continue # No observation + + for iz1 in range(len(zlist[t1][:, 0])): + for iz2 in range(len(zlist[t2][:, 0])): + if zlist[t1][iz1, 3] == zlist[t2][iz2, 3]: + d1 = zlist[t1][iz1, 0] + angle1, phi1 = zlist[t1][iz1, 1], zlist[t1][iz1, 2] + d2 = zlist[t2][iz2, 0] + angle2, phi2 = zlist[t2][iz2, 1], zlist[t2][iz2, 2] + + edge = calc_edge(x1, y1, yaw1, x2, y2, yaw2, d1, + angle1, phi1, d2, angle2, phi2, t1, t2) + + edges.append(edge) + cost += (edge.e.T * edge.omega * edge.e)[0, 0] + + print("cost:", cost, ",nedge:", len(edges)) + return edges + + +def calc_jacobian(edge): + t1 = edge.yaw1 + edge.angle1 + A = np.matrix([[-1.0, 0, edge.d1 * math.sin(t1)], + [0, -1.0, -edge.d1 * math.cos(t1)], + [0, 0, -1.0]]) + + t2 = edge.yaw2 + edge.angle2 + B = np.matrix([[1.0, 0, -edge.d2 * math.sin(t2)], + [0, 1.0, edge.d2 * math.cos(t2)], + [0, 0, 1.0]]) + + return A, B + + +def fill_H_and_b(H, b, edge): + + A, B = calc_jacobian(edge) + + id1 = edge.id1 * STATE_SIZE + id2 = edge.id2 * STATE_SIZE + + H[id1:id1 + STATE_SIZE, id1:id1 + STATE_SIZE] += A.T * edge.omega * A + H[id1:id1 + STATE_SIZE, id2:id2 + STATE_SIZE] += A.T * edge.omega * B + H[id2:id2 + STATE_SIZE, id1:id1 + STATE_SIZE] += B.T * edge.omega * A + H[id2:id2 + STATE_SIZE, id2:id2 + STATE_SIZE] += B.T * edge.omega * B + + b[id1:id1 + STATE_SIZE, 0] += (A.T * edge.omega * edge.e) + b[id2:id2 + STATE_SIZE, 0] += (B.T * edge.omega * edge.e) + + return H, b + + +def graph_based_slam(x_init, hz): + print("start graph based slam") + + zlist = copy.deepcopy(hz) + zlist.insert(1, zlist[0]) + + x_opt = copy.deepcopy(x_init) + nt = x_opt.shape[1] + n = nt * STATE_SIZE + + for itr in range(MAX_ITR): + edges = calc_edges(x_opt, zlist) + + H = np.matrix(np.zeros((n, n))) + b = np.matrix(np.zeros((n, 1))) + + for edge in edges: + H, b = fill_H_and_b(H, b, edge) + + # to fix origin + H[0:STATE_SIZE, 0:STATE_SIZE] += np.identity(STATE_SIZE) + + dx = - np.linalg.inv(H).dot(b) + + for i in range(nt): + x_opt[0:3, i] += dx[i * 3:i * 3 + 3, 0] + + diff = dx.T.dot(dx) + print("iteration: %d, diff: %f" % (itr + 1, diff)) + if diff < 1.0e-5: + break + + return x_opt + + +def calc_input(): + v = 1.0 # [m/s] + yawrate = 0.1 # [rad/s] + u = np.matrix([v, yawrate]).T + return u + + +def observation(xTrue, xd, u, RFID): + + xTrue = motion_model(xTrue, u) + + # add noise to gps x-y + z = np.matrix(np.zeros((0, 4))) + + for i in range(len(RFID[:, 0])): + + dx = RFID[i, 0] - xTrue[0, 0] + dy = RFID[i, 1] - xTrue[1, 0] + d = math.sqrt(dx**2 + dy**2) + angle = pi_2_pi(math.atan2(dy, dx)) - xTrue[2, 0] + phi = pi_2_pi(math.atan2(dy, dx)) + if d <= MAX_RANGE: + dn = d + np.random.randn() * Qsim[0, 0] # add noise + anglen = angle + np.random.randn() * Qsim[1, 1] # add noise + zi = np.matrix([dn, anglen, phi, i]) + z = np.vstack((z, zi)) + + # add noise to input + ud1 = u[0, 0] + np.random.randn() * Rsim[0, 0] + ud2 = u[1, 0] + np.random.randn() * Rsim[1, 1] + ud = np.matrix([ud1, ud2]).T + + xd = motion_model(xd, ud) + + return xTrue, z, xd, ud + + +def motion_model(x, u): + + F = np.matrix([[1.0, 0, 0], + [0, 1.0, 0], + [0, 0, 1.0]]) + + B = np.matrix([[DT * math.cos(x[2, 0]), 0], + [DT * math.sin(x[2, 0]), 0], + [0.0, DT]]) + + x = F * x + B * u + + return x + + +def pi_2_pi(angle): + return (angle + math.pi) % (2*math.pi) - math.pi + + +def main(): + print(__file__ + " start!!") + + time = 0.0 + + # RFID positions [x, y, yaw] + RFID = np.array([[10.0, -2.0, 0.0], + [15.0, 10.0, 0.0], + [3.0, 15.0, 0.0], + [-5.0, 20.0, 0.0], + [-5.0, 5.0, 0.0] + ]) + + # State Vector [x y yaw v]' + xTrue = np.matrix(np.zeros((STATE_SIZE, 1))) + xDR = np.matrix(np.zeros((STATE_SIZE, 1))) # Dead reckoning + + # history + hxTrue = xTrue + hxDR = xTrue + hz = [] + dtime = 0.0 + + while SIM_TIME >= time: + time += DT + dtime += DT + u = calc_input() + + xTrue, z, xDR, ud = observation(xTrue, xDR, u, RFID) + + hxDR = np.hstack((hxDR, xDR)) + hxTrue = np.hstack((hxTrue, xTrue)) + hz.append(z) + + if dtime >= show_graph_dtime: + x_opt = graph_based_slam(hxDR, hz) + dtime = 0.0 + + if show_animation: + plt.cla() + + plt.plot(RFID[:, 0], RFID[:, 1], "*k") + + plt.plot(np.array(hxTrue[0, :]).flatten(), + np.array(hxTrue[1, :]).flatten(), "-b") + plt.plot(np.array(hxDR[0, :]).flatten(), + np.array(hxDR[1, :]).flatten(), "-k") + plt.plot(np.array(x_opt[0, :]).flatten(), + np.array(x_opt[1, :]).flatten(), "-r") + plt.axis("equal") + plt.grid(True) + plt.title("Time" + str(time)[0:5]) + plt.pause(1.0) + + +if __name__ == '__main__': + main() diff --git a/_config.yml b/_config.yml index c7418817..3ce0be72 100644 --- a/_config.yml +++ b/_config.yml @@ -1 +1,2 @@ -theme: jekyll-theme-slate \ No newline at end of file +theme: jekyll-theme-slate +show_downloads: true diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..9296811e --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = PythonRobotics +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..de982ed4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,25 @@ +## Python Robotics Documentation + +This folder contains documentation for the Python Robotics project. + + +### Build the Documentation + +#### Install Sphinx and Theme + +``` +pip install sphinx sphinx-autobuild sphinx-rtd-theme +``` + +#### Building the Docs + +In the `docs/` folder: +``` +make html +``` + +if you want to building each time a file is changed: + +``` +sphinx-autobuild . _build/html +``` diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..20bcca12 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'PythonRobotics' +copyright = '2018, Atsushi Sakai' +author = 'Atsushi Sakai' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# Fix for read the docs +on_rtd = os.environ.get('READTHEDOCS') == 'True' +if on_rtd: + html_theme = 'default' +else: + html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +html_logo = '../icon.png' +html_theme_options = { + 'display_version': False, +} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PythonRoboticsdoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'PythonRobotics.tex', 'PythonRobotics Documentation', + 'Atsushi Sakai', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pythonrobotics', 'PythonRobotics Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'PythonRobotics', 'PythonRobotics Documentation', + author, 'PythonRobotics', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Extension configuration ------------------------------------------------- diff --git a/docs/getting_started.rst b/docs/getting_started.rst new file mode 100644 index 00000000..153c3d9c --- /dev/null +++ b/docs/getting_started.rst @@ -0,0 +1,29 @@ +.. _getting_started: + +Getting Started +=============== + +Requirements +------------- + +- Python 3.6.x +- numpy +- scipy +- matplotlib +- pandas +- `cvxpy`_ + +.. _cvxpy: http://www.cvxpy.org/en/latest/ + + +How to use +---------- + +1. Install the required libraries. You can use environment.yml with + conda command. + +2. Clone this repo. + +3. Execute python script in each directory. + +4. Add star to this repo if you like it 😃. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..896cbd3c --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,50 @@ +.. PythonRobotics documentation master file, created by + sphinx-quickstart on Sat Sep 15 13:15:55 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to PythonRobotics's documentation! +========================================== + +Python codes for robotics algorithm. The project is on `GitHub`_. + +This is a Python code collection of robotics algorithms, especially for autonomous navigation. + +Features: + +1. Easy to read for understanding each algorithm's basic idea. + +2. Widely used and practical algorithms are selected. + +3. Minimum dependency. + +See this paper for more details: + +- `[1808.10703] PythonRobotics: a Python code collection of robotics + algorithms`_ (`BibTeX`_) + +.. _`[1808.10703] PythonRobotics: a Python code collection of robotics algorithms`: https://arxiv.org/abs/1808.10703 +.. _BibTeX: https://github.com/AtsushiSakai/PythonRoboticsPaper/blob/master/python_robotics.bib +.. _GitHub: https://github.com/AtsushiSakai/PythonRobotics + + +.. toctree:: + :maxdepth: 2 + :caption: Guide + + getting_started + modules/localization + modules/mapping + modules/slam + modules/path_planning + modules/path_tracking + modules/arm_navigation + modules/aerial_navigation + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..6aab964d --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=PythonRobotics + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/modules/aerial_navigation.rst b/docs/modules/aerial_navigation.rst new file mode 100644 index 00000000..2f15289b --- /dev/null +++ b/docs/modules/aerial_navigation.rst @@ -0,0 +1,13 @@ +.. _aerial_navigation: + +Aerial Navigation +================= + +Drone 3d trajectory following +----------------------------- + +This is a 3d trajectory following simulation for a quadrotor. + +|3| + +.. |3| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/AerialNavigation/drone_3d_trajectory_following/animation.gif diff --git a/docs/modules/arm_navigation.rst b/docs/modules/arm_navigation.rst new file mode 100644 index 00000000..a3e13dce --- /dev/null +++ b/docs/modules/arm_navigation.rst @@ -0,0 +1,33 @@ +.. _arm_navigation: + +Arm Navigation +============== + +Two joint arm to point control +------------------------------ + +Two joint arm to a point control simulation. + +This is a interactive simulation. + +You can set the goal position of the end effector with left-click on the +ploting area. + +|3| + +N joint arm to point control +---------------------------- + +N joint arm to a point control simulation. + +This is a interactive simulation. + +You can set the goal position of the end effector with left-click on the +ploting area. + +|4| + +In this simulation N = 10, however, you can change it. + +.. |3| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/ArmNavigation/two_joint_arm_to_point_control/animation.gif +.. |4| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/ArmNavigation/n_joint_arm_to_point_control/animation.gif diff --git a/docs/modules/localization.rst b/docs/modules/localization.rst new file mode 100644 index 00000000..a69ab51c --- /dev/null +++ b/docs/modules/localization.rst @@ -0,0 +1,89 @@ +.. _localization: + +Localization +============ + +Extended Kalman Filter localization +----------------------------------- + +.. raw:: html + + + +This is a sensor fusion localization with Extended Kalman Filter(EKF). + +The blue line is true trajectory, the black line is dead reckoning +trajectory, + +the green point is positioning observation (ex. GPS), and the red line +is estimated trajectory with EKF. + +The red ellipse is estimated covariance ellipse with EKF. + +Ref: + +- `PROBABILISTIC ROBOTICS`_ + +Unscented Kalman Filter localization +------------------------------------ + +|2| + +This is a sensor fusion localization with Unscented Kalman Filter(UKF). + +The lines and points are same meaning of the EKF simulation. + +Ref: + +- `Discriminatively Trained Unscented Kalman Filter for Mobile Robot + Localization`_ + +Particle filter localization +---------------------------- + +|3| + +This is a sensor fusion localization with Particle Filter(PF). + +The blue line is true trajectory, the black line is dead reckoning +trajectory, + +and the red line is estimated trajectory with PF. + +It is assumed that the robot can measure a distance from landmarks +(RFID). + +This measurements are used for PF localization. + +Ref: + +- `PROBABILISTIC ROBOTICS`_ + +Histogram filter localization +----------------------------- + +|4| + +This is a 2D localization example with Histogram filter. + +The red cross is true position, black points are RFID positions. + +The blue grid shows a position probability of histogram filter. + +In this simulation, x,y are unknown, yaw is known. + +The filter integrates speed input and range observations from RFID for +localization. + +Initial position is not needed. + +Ref: + +- `PROBABILISTIC ROBOTICS`_ + +.. _PROBABILISTIC ROBOTICS: http://www.probabilistic-robotics.org/ +.. _Discriminatively Trained Unscented Kalman Filter for Mobile Robot Localization: https://www.researchgate.net/publication/267963417_Discriminatively_Trained_Unscented_Kalman_Filter_for_Mobile_Robot_Localization + +.. |2| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/Localization/unscented_kalman_filter/animation.gif +.. |3| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/Localization/particle_filter/animation.gif +.. |4| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/Localization/histogram_filter/animation.gif diff --git a/docs/modules/mapping.rst b/docs/modules/mapping.rst new file mode 100644 index 00000000..9da79fd6 --- /dev/null +++ b/docs/modules/mapping.rst @@ -0,0 +1,43 @@ +.. _mapping: + +Mapping +======= + +Gaussian grid map +----------------- + +This is a 2D Gaussian grid mapping example. + +|2| + +Ray casting grid map +-------------------- + +This is a 2D ray casting grid mapping example. + +|3| + +k-means object clustering +------------------------- + +This is a 2D object clustering with k-means algorithm. + +|4| + +Object shape recognition using circle fitting +--------------------------------------------- + +This is an object shape recognition using circle fitting. + +|5| + +The blue circle is the true object shape. + +The red crosses are observations from a ranging sensor. + +The red circle is the estimated object shape using circle fitting. + +.. |2| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/Mapping/gaussian_grid_map/animation.gif +.. |3| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/Mapping/raycasting_grid_map/animation.gif +.. |4| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/Mapping/kmeans_clustering/animation.gif +.. |5| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/Mapping/circle_fitting/animation.gif diff --git a/docs/modules/path_planning.rst b/docs/modules/path_planning.rst new file mode 100644 index 00000000..dcec3e39 --- /dev/null +++ b/docs/modules/path_planning.rst @@ -0,0 +1,456 @@ +.. _path_planning: + +Path Planning +============= + +Dynamic Window Approach +----------------------- + +This is a 2D navigation sample code with Dynamic Window Approach. + +- `The Dynamic Window Approach to Collision + Avoidance `__ + +|DWA| + +Grid based search +----------------- + +Dijkstra algorithm +~~~~~~~~~~~~~~~~~~ + +This is a 2D grid based shortest path planning with Dijkstra's +algorithm. + +|Dijkstra| + +In the animation, cyan points are searched nodes. + +.. _a*-algorithm: + +A\* algorithm +~~~~~~~~~~~~~ + +This is a 2D grid based shortest path planning with A star algorithm. + +|astar| + +In the animation, cyan points are searched nodes. + +Its heuristic is 2D Euclid distance. + +Potential Field algorithm +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is a 2D grid based path planning with Potential Field algorithm. + +|PotentialField| + +In the animation, the blue heat map shows potential value on each grid. + +Ref: + +- `Robotic Motion Planning:Potential + Functions `__ + +Model Predictive Trajectory Generator +------------------------------------- + +This is a path optimization sample on model predictive trajectory +generator. + +This algorithm is used for state lattice planner. + +Path optimization sample +~~~~~~~~~~~~~~~~~~~~~~~~ + +|4| + +Lookup table generation sample +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +|5| + +Ref: + +- `Optimal rough terrain trajectory generation for wheeled mobile + robots `__ + +State Lattice Planning +---------------------- + +This script is a path planning code with state lattice planning. + +This code uses the model predictive trajectory generator to solve +boundary problem. + +Ref: + +- `Optimal rough terrain trajectory generation for wheeled mobile + robots `__ + +- `State Space Sampling of Feasible Motions for High-Performance Mobile + Robot Navigation in Complex + Environments `__ + +Uniform polar sampling +~~~~~~~~~~~~~~~~~~~~~~ + +|6| + +Biased polar sampling +~~~~~~~~~~~~~~~~~~~~~ + +|7| + +Lane sampling +~~~~~~~~~~~~~ + +|8| + +.. _probabilistic-road-map-(prm)-planning: + +Probabilistic Road-Map (PRM) planning +------------------------------------- + +|PRM| + +This PRM planner uses Dijkstra method for graph search. + +In the animation, blue points are sampled points, + +Cyan crosses means searched points with Dijkstra method, + +The red line is the final path of PRM. + +Ref: + +- `Probabilistic roadmap - + Wikipedia `__ + +   + +Voronoi Road-Map planning +------------------------- + +|VRM| + +This Voronoi road-map planner uses Dijkstra method for graph search. + +In the animation, blue points are Voronoi points, + +Cyan crosses mean searched points with Dijkstra method, + +The red line is the final path of Vornoi Road-Map. + +Ref: + +- `Robotic Motion + Planning `__ + +.. _rapidly-exploring-random-trees-(rrt): + +Rapidly-Exploring Random Trees (RRT) +------------------------------------ + +Basic RRT +~~~~~~~~~ + +|9| + +This is a simple path planning code with Rapidly-Exploring Random Trees +(RRT) + +Black circles are obstacles, green line is a searched tree, red crosses +are start and goal positions. + +.. _rrt*: + +RRT\* +~~~~~ + +|10| + +This is a path planning code with RRT\* + +Black circles are obstacles, green line is a searched tree, red crosses +are start and goal positions. + +Ref: + +- `Incremental Sampling-based Algorithms for Optimal Motion + Planning `__ + +- `Sampling-based Algorithms for Optimal Motion + Planning `__ + +RRT with dubins path +~~~~~~~~~~~~~~~~~~~~ + +|PythonRobotics| + +Path planning for a car robot with RRT and dubins path planner. + +.. _rrt*-with-dubins-path: + +RRT\* with dubins path +~~~~~~~~~~~~~~~~~~~~~~ + +|AtsushiSakai/PythonRobotics| + +Path planning for a car robot with RRT\* and dubins path planner. + +.. _rrt*-with-reeds-sheep-path: + +RRT\* with reeds-sheep path +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +|11| + +Path planning for a car robot with RRT\* and reeds sheep path planner. + +.. _informed-rrt*: + +Informed RRT\* +~~~~~~~~~~~~~~ + +|irrt| + +This is a path planning code with Informed RRT*. + +The cyan ellipse is the heuristic sampling domain of Informed RRT*. + +Ref: + +- `Informed RRT\*: Optimal Sampling-based Path Planning Focused via + Direct Sampling of an Admissible Ellipsoidal + Heuristic `__ + +.. _batch-informed-rrt*: + +Batch Informed RRT\* +~~~~~~~~~~~~~~~~~~~~ + +|irrt2| + +This is a path planning code with Batch Informed RRT*. + +Ref: + +- `Batch Informed Trees (BIT*): Sampling-based Optimal Planning via the + Heuristically Guided Search of Implicit Random Geometric + Graphs `__ + +.. _closed-loop-rrt*: + +Closed Loop RRT\* +~~~~~~~~~~~~~~~~~ + +A vehicle model based path planning with closed loop RRT*. + +|CLRRT| + +In this code, pure-pursuit algorithm is used for steering control, + +PID is used for speed control. + +Ref: + +- `Motion Planning in Complex Environments using Closed-loop + Prediction `__ + +- `Real-time Motion Planning with Applications to Autonomous Urban + Driving `__ + +- `[1601.06326] Sampling-based Algorithms for Optimal Motion Planning + Using Closed-loop Prediction `__ + +.. _lqr-rrt*: + +LQR-RRT\* +~~~~~~~~~ + +This is a path planning simulation with LQR-RRT*. + +A double integrator motion model is used for LQR local planner. + +|LQRRRT| + +Ref: + +- `LQR-RRT\*: Optimal Sampling-Based Motion Planning with Automatically + Derived Extension + Heuristics `__ + +- `MahanFathi/LQR-RRTstar: LQR-RRT\* method is used for random motion + planning of a simple pendulum in its phase + plot `__ + +Cubic spline planning +--------------------- + +A sample code for cubic path planning. + +This code generates a curvature continuous path based on x-y waypoints +with cubic spline. + +Heading angle of each point can be also calculated analytically. + +|12| +|13| +|14| + +B-Spline planning +----------------- + +|B-Spline| + +This is a path planning with B-Spline curse. + +If you input waypoints, it generates a smooth path with B-Spline curve. + +The final course should be on the first and last waypoints. + +Ref: + +- `B-spline - Wikipedia `__ + +.. _eta^3-spline-path-planning: + +Eta^3 Spline path planning +-------------------------- + +|eta3| + +This is a path planning with Eta^3 spline. + +Ref: + +- `\\eta^3-Splines for the Smooth Path Generation of Wheeled Mobile + Robots `__ + +Bezier path planning +-------------------- + +A sample code of Bezier path planning. + +It is based on 4 control points Beier path. + +|Bezier1| + +If you change the offset distance from start and end point, + +You can get different Beizer course: + +|Bezier2| + +Ref: + +- `Continuous Curvature Path Generation Based on Bezier Curves for + Autonomous + Vehicles `__ + +Quintic polynomials planning +---------------------------- + +Motion planning with quintic polynomials. + +|2| + +It can calculate 2D path, velocity, and acceleration profile based on +quintic polynomials. + +Ref: + +- `Local Path Planning And Motion Control For Agv In + Positioning `__ + +Dubins path planning +-------------------- + +A sample code for Dubins path planning. + +|dubins| + +Ref: + +- `Dubins path - + Wikipedia `__ + +Reeds Shepp planning +-------------------- + +A sample code with Reeds Shepp path planning. + +|RSPlanning| + +Ref: + +- `15.3.2 Reeds-Shepp + Curves `__ + +- `optimal paths for a car that goes both forwards and + backwards `__ + +- `ghliu/pyReedsShepp: Implementation of Reeds Shepp + curve. `__ + +LQR based path planning +----------------------- + +A sample code using LQR based path planning for double integrator model. + +|RSPlanning2| + +Optimal Trajectory in a Frenet Frame +------------------------------------ + +|15| + +This is optimal trajectory generation in a Frenet Frame. + +The cyan line is the target course and black crosses are obstacles. + +The red line is predicted path. + +Ref: + +- `Optimal Trajectory Generation for Dynamic Street Scenarios in a + Frenet + Frame `__ + +- `Optimal trajectory generation for dynamic street scenarios in a + Frenet Frame `__ + +.. |DWA| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/DynamicWindowApproach/animation.gif +.. |Dijkstra| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/Dijkstra/animation.gif +.. |astar| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/AStar/animation.gif +.. |PotentialField| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/PotentialFieldPlanning/animation.gif +.. |4| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/ModelPredictiveTrajectoryGenerator/kn05animation.gif +.. |5| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/ModelPredictiveTrajectoryGenerator/lookuptable.png?raw=True +.. |6| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/StateLatticePlanner/UniformPolarSampling.gif +.. |7| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/StateLatticePlanner/BiasedPolarSampling.gif +.. |8| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/StateLatticePlanner/LaneSampling.gif +.. |PRM| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/ProbabilisticRoadMap/animation.gif +.. |VRM| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/VoronoiRoadMap/animation.gif +.. |9| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/RRT/animation.gif +.. |10| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/RRTstar/animation.gif +.. |PythonRobotics| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/RRTDubins/animation.gif +.. |AtsushiSakai/PythonRobotics| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/RRTStarDubins/animation.gif +.. |11| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/RRTStarReedsShepp/animation.gif +.. |irrt| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/InformedRRTStar/animation.gif +.. |irrt2| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/BatchInformedRRTStar/animation.gif +.. |CLRRT| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/ClosedLoopRRTStar/animation.gif +.. |LQRRRT| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/LQRRRTStar/animation.gif +.. |12| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/CubicSpline/Figure_1.png?raw=True +.. |13| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/CubicSpline/Figure_2.png?raw=True +.. |14| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/CubicSpline/Figure_3.png?raw=True +.. |B-Spline| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/BSplinePath/Figure_1.png?raw=True +.. |eta3| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/Eta3SplinePath/animation.gif?raw=True +.. |Bezier1| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/BezierPath/Figure_1.png?raw=True +.. |Bezier2| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/BezierPath/Figure_2.png?raw=True +.. |2| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/QuinticPolynomialsPlanner/animation.gif +.. |dubins| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/DubinsPath/animation.gif?raw=True +.. |RSPlanning| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/ReedsSheppPath/animation.gif?raw=true +.. |RSPlanning2| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/LQRPlanner/animation.gif?raw=true +.. |15| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathPlanning/FrenetOptimalTrajectory/animation.gif diff --git a/docs/modules/path_tracking.rst b/docs/modules/path_tracking.rst new file mode 100644 index 00000000..24e6c9c5 --- /dev/null +++ b/docs/modules/path_tracking.rst @@ -0,0 +1,112 @@ +.. _path_tracking: + +Path tracking +============= + +move to a pose control +---------------------- + +This is a simulation of moving to a pose control + +|2| + +Ref: + +- `P. I. Corke, "Robotics, Vision and Control" \| SpringerLink + p102 `__ + +Pure pursuit tracking +--------------------- + +Path tracking simulation with pure pursuit steering control and PID +speed control. + +|3| + +The red line is a target course, the green cross means the target point +for pure pursuit control, the blue line is the tracking. + +Ref: + +- `A Survey of Motion Planning and Control Techniques for Self-driving + Urban Vehicles `__ + +Stanley control +--------------- + +Path tracking simulation with Stanley steering control and PID speed +control. + +|4| + +Ref: + +- `Stanley: The robot that won the DARPA grand + challenge `__ + +- `Automatic Steering Methods for Autonomous Automobile Path + Tracking `__ + +Rear wheel feedback control +--------------------------- + +Path tracking simulation with rear wheel feedback steering control and +PID speed control. + +|5| + +Ref: + +- `A Survey of Motion Planning and Control Techniques for Self-driving + Urban Vehicles `__ + +.. _linearquadratic-regulator-(lqr)-steering-control: + +Linear–quadratic regulator (LQR) steering control +------------------------------------------------- + +Path tracking simulation with LQR steering control and PID speed +control. + +|6| + +Ref: + +- `ApolloAuto/apollo: An open autonomous driving + platform `__ + +.. _linearquadratic-regulator-(lqr)-speed-and-steering-control: + +Linear–quadratic regulator (LQR) speed and steering control +----------------------------------------------------------- + +Path tracking simulation with LQR speed and steering control. + +|7| + +Ref: + +- `Towards fully autonomous driving: Systems and algorithms - IEEE + Conference + Publication `__ + +Model predictive speed and steering control +------------------------------------------- + +Path tracking simulation with iterative linear model predictive speed +and steering control. + +.. raw:: html + + + +Ref: + +- `notebook `__ + +.. |2| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathTracking/move_to_pose/animation.gif +.. |3| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathTracking/pure_pursuit/animation.gif +.. |4| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathTracking/stanley_controller/animation.gif +.. |5| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathTracking/rear_wheel_feedback/animation.gif +.. |6| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathTracking/lqr_steer_control/animation.gif +.. |7| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/PathTracking/lqr_speed_steer_control/animation.gif diff --git a/docs/modules/slam.rst b/docs/modules/slam.rst new file mode 100644 index 00000000..13a122d8 --- /dev/null +++ b/docs/modules/slam.rst @@ -0,0 +1,104 @@ +.. _slam: + +SLAM +==== + +Simultaneous Localization and Mapping(SLAM) examples + +.. _iterative-closest-point-(icp)-matching: + +Iterative Closest Point (ICP) Matching +-------------------------------------- + +This is a 2D ICP matching example with singular value decomposition. + +It can calculate a rotation matrix and a translation vector between +points to points. + +|3| + +Ref: + +- `Introduction to Mobile Robotics: Iterative Closest Point Algorithm`_ + +EKF SLAM +-------- + +This is an Extended Kalman Filter based SLAM example. + +The blue line is ground truth, the black line is dead reckoning, the red +line is the estimated trajectory with EKF SLAM. + +The green crosses are estimated landmarks. + +|4| + +Ref: + +- `PROBABILISTIC ROBOTICS`_ + +FastSLAM 1.0 +------------ + +This is a feature based SLAM example using FastSLAM 1.0. + +The blue line is ground truth, the black line is dead reckoning, the red +line is the estimated trajectory with FastSLAM. + +The red points are particles of FastSLAM. + +Black points are landmarks, blue crosses are estimated landmark +positions by FastSLAM. + +|5| + +Ref: + +- `PROBABILISTIC ROBOTICS`_ + +- `SLAM simulations by Tim Bailey`_ + +FastSLAM 2.0 +------------ + +This is a feature based SLAM example using FastSLAM 2.0. + +The animation has the same meanings as one of FastSLAM 1.0. + +|6| + +Ref: + +- `PROBABILISTIC ROBOTICS`_ + +- `SLAM simulations by Tim Bailey`_ + +Graph based SLAM +---------------- + +This is a graph based SLAM example. + +The blue line is ground truth. + +The black line is dead reckoning. + +The red line is the estimated trajectory with Graph based SLAM. + +The black stars are landmarks for graph edge generation. + +|7| + +Ref: + +- `A Tutorial on Graph-Based SLAM`_ + +.. _`Introduction to Mobile Robotics: Iterative Closest Point Algorithm`: https://cs.gmu.edu/~kosecka/cs685/cs685-icp.pdf +.. _PROBABILISTIC ROBOTICS: http://www.probabilistic-robotics.org/ +.. _SLAM simulations by Tim Bailey: http://www-personal.acfr.usyd.edu.au/tbailey/software/slam_simulations.htm +.. _A Tutorial on Graph-Based SLAM: http://www2.informatik.uni-freiburg.de/~stachnis/pdf/grisetti10titsmag.pdf + +.. |3| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/SLAM/iterative_closest_point/animation.gif +.. |4| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/SLAM/EKFSLAM/animation.gif +.. |5| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/SLAM/FastSLAM1/animation.gif +.. |6| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/SLAM/FastSLAM2/animation.gif +.. |7| image:: https://github.com/AtsushiSakai/PythonRobotics/raw/master/SLAM/GraphBasedSLAM/animation.gif diff --git a/environment.yml b/environment.yml new file mode 100644 index 00000000..3d7ff289 --- /dev/null +++ b/environment.yml @@ -0,0 +1,9 @@ +name: python_robotics +dependencies: +- python==3.6 +- matplotlib +- scipy +- numpy +- pandas +- pip: + - cvxpy diff --git a/paper/.gitkeep b/paper/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..1398664b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +numpy +pandas +scipy +matplotlib +cvxpy diff --git a/tests/test_LQR_planner.py b/tests/test_LQR_planner.py new file mode 100644 index 00000000..5ece27f8 --- /dev/null +++ b/tests/test_LQR_planner.py @@ -0,0 +1,15 @@ +from unittest import TestCase + +import sys +sys.path.append("./PathPlanning/LQRPlanner") + +from PathPlanning.LQRPlanner import LQRplanner as m + +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_batch_informed_rrt_star.py b/tests/test_batch_informed_rrt_star.py new file mode 100644 index 00000000..cb245c54 --- /dev/null +++ b/tests/test_batch_informed_rrt_star.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from PathPlanning.BatchInformedRRTStar import batch_informed_rrtstar as m + +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_circle_fitting.py b/tests/test_circle_fitting.py new file mode 100644 index 00000000..d1845482 --- /dev/null +++ b/tests/test_circle_fitting.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from Mapping.circle_fitting import circle_fitting as m + +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_drone_3d_trajectory_following.py b/tests/test_drone_3d_trajectory_following.py new file mode 100644 index 00000000..194a3299 --- /dev/null +++ b/tests/test_drone_3d_trajectory_following.py @@ -0,0 +1,14 @@ +from unittest import TestCase + +import sys +sys.path.append("./AerialNavigation/drone_3d_trajectory_following/") + +from AerialNavigation.drone_3d_trajectory_following import drone_3d_trajectory_following as m +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_eta3_spline_path.py b/tests/test_eta3_spline_path.py new file mode 100644 index 00000000..33a4223d --- /dev/null +++ b/tests/test_eta3_spline_path.py @@ -0,0 +1,10 @@ + +from unittest import TestCase +from PathPlanning.Eta3SplinePath import eta3_spline_path as m + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_fast_slam1.py b/tests/test_fast_slam1.py new file mode 100644 index 00000000..477b795d --- /dev/null +++ b/tests/test_fast_slam1.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from SLAM.FastSLAM1 import fast_slam1 as m + +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_fast_slam2.py b/tests/test_fast_slam2.py new file mode 100644 index 00000000..48c2ba99 --- /dev/null +++ b/tests/test_fast_slam2.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from SLAM.FastSLAM2 import fast_slam2 as m + +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_graph_based_slam.py b/tests/test_graph_based_slam.py new file mode 100644 index 00000000..13524366 --- /dev/null +++ b/tests/test_graph_based_slam.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from SLAM.GraphBasedSLAM import graph_based_slam as m + +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_histogram_filter.py b/tests/test_histogram_filter.py new file mode 100644 index 00000000..f39995b2 --- /dev/null +++ b/tests/test_histogram_filter.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from Localization.histogram_filter import histogram_filter as m + +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_informed_rrt_star.py b/tests/test_informed_rrt_star.py new file mode 100644 index 00000000..a10b3e02 --- /dev/null +++ b/tests/test_informed_rrt_star.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from PathPlanning.InformedRRTStar import informed_rrt_star as m + +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_kmeans_clustering.py b/tests/test_kmeans_clustering.py new file mode 100644 index 00000000..b5b2cb7a --- /dev/null +++ b/tests/test_kmeans_clustering.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from Mapping.kmeans_clustering import kmeans_clustering as m + +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_lqr_rrt_star.py b/tests/test_lqr_rrt_star.py new file mode 100644 index 00000000..17b363c0 --- /dev/null +++ b/tests/test_lqr_rrt_star.py @@ -0,0 +1,15 @@ +from unittest import TestCase + +import sys +sys.path.append("./PathPlanning/LQRRRTStar/") + +from PathPlanning.LQRRRTStar import lqr_rrt_star as m + +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_model_predictive_speed_and_steer_control.py b/tests/test_model_predictive_speed_and_steer_control.py index 07c4d6c8..86ed0f24 100644 --- a/tests/test_model_predictive_speed_and_steer_control.py +++ b/tests/test_model_predictive_speed_and_steer_control.py @@ -13,3 +13,4 @@ class Test(TestCase): def test1(self): m.show_animation = False m.main() + m.main2() diff --git a/tests/test_move_to_pose.py b/tests/test_move_to_pose.py new file mode 100644 index 00000000..01257920 --- /dev/null +++ b/tests/test_move_to_pose.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from PathTracking.move_to_pose import move_to_pose as m + +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.main() diff --git a/tests/test_n_joint_arm_to_point_control.py b/tests/test_n_joint_arm_to_point_control.py new file mode 100644 index 00000000..e3988355 --- /dev/null +++ b/tests/test_n_joint_arm_to_point_control.py @@ -0,0 +1,14 @@ +from unittest import TestCase + +import sys +sys.path.append("./ArmNavigation/n_joint_arm_to_point_control/") + +from ArmNavigation.n_joint_arm_to_point_control import n_joint_arm_to_point_control as m +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.animation() diff --git a/tests/test_two_joint_arm_to_point_control.py b/tests/test_two_joint_arm_to_point_control.py new file mode 100644 index 00000000..e2b9c21a --- /dev/null +++ b/tests/test_two_joint_arm_to_point_control.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from ArmNavigation.two_joint_arm_to_point_control import two_joint_arm_to_point_control as m + +print(__file__) + + +class Test(TestCase): + + def test1(self): + m.show_animation = False + m.animation() diff --git a/users_comments.md b/users_comments.md new file mode 100644 index 00000000..02503cf1 --- /dev/null +++ b/users_comments.md @@ -0,0 +1,36 @@ + +# Users comments + +These are comments from user's using [![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/AtsushiSakai) + +--- + +Dear AtsushiSakai,
thank you for building this cool repo for robotics.
I am still learning robotics and this will give me a good starting point.
I hope this note gets to you.

+ +--David Senicic + +--- + +Dear AtsushiSakai,
WE found your project from google when first searching for a rosbag to csv converter. We are a small team in MCity working on Connected (and Automated) vehicles. We are working with an opensource platform that serve as the AI to control the vehicle. Your code and documentation helped us a lot!

Thank you! Have a nice day! + +--Hanlin(Julia) Chen, MCity Apollo team + +--- + +Dear Atsushi Sakai,

With your simplistic descriptions in text and as gifs, i was able to help my students understand easily and effectively, I would be honored to help, in anyway towards this, As a hobbyist have been developing lidar based slam navigation systems from 2011 and its only right to acknowledge and thank you for your contributions. + +--Shiva + +--- + +Dear Atsushi Sakai
I first came across your Matlab repository on ICP and SLAM. The repository for both python and Matlab helped me in understanding the essential concepts of robotics.I am really grateful to you for helping me develop my understanding of the concepts of robotics. + +--Sparsh Garg + +--- + +Dear AtsushiSakai,
Thank you very much for all the example programs related to Robotics + +--Kalyan + +---