Skip to content

Commit 680ecda

Browse files
Add a Robot class for Move to Pose Algorithm (AtsushiSakai#596)
* Added speed limitation of the robot * Removed leading underscores for global vars * Added unit test for robot speed limitation * Modified x/abs(x) to np.sign(x); fixed code style * Removed 'random' from test func header comment * Added Robot class for move to pose * Revert * Added Robot class for move to pose * Added a type annotation for Robot class * Fixed the annotaion comment * Moved instance varaible outside of the Robot class * Fixed code style Python 3.9 CI * Removed whitespaces from the last line * Applied PR AtsushiSakai#596 change requests * Fixed typos * Update Control/move_to_pose/move_to_pose_robot_class.py Co-authored-by: Atsushi Sakai <[email protected]> * Moved PathFinderController class to move_to_pose * Fixed issue AtsushiSakai#600 * Added update_command() to PathFinderController * Removed trailing whitespaces * Updated move to pose doc * Added code and doc comments * Updated unit test * Removed trailing whitespaces * Removed more trailing whitespaces Co-authored-by: Atsushi Sakai <[email protected]>
1 parent 4e1f644 commit 680ecda

File tree

4 files changed

+424
-30
lines changed

4 files changed

+424
-30
lines changed

Control/move_to_pose/move_to_pose.py

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
Move to specified pose
44
55
Author: Daniel Ingram (daniel-s-ingram)
6-
Atsushi Sakai(@Atsushi_twi)
6+
Atsushi Sakai (@Atsushi_twi)
7+
Seied Muhammad Yazdian (@Muhammad-Yazdian)
78
89
P. I. Corke, "Robotics, Vision & Control", Springer 2017, ISBN 978-3-319-54413-7
910
@@ -13,10 +14,77 @@
1314
import numpy as np
1415
from random import random
1516

17+
18+
class PathFinderController:
19+
"""
20+
Constructs an instantiate of the PathFinderController for navigating a
21+
3-DOF wheeled robot on a 2D plane
22+
23+
Parameters
24+
----------
25+
Kp_rho : The linear velocity gain to translate the robot along a line
26+
towards the goal
27+
Kp_alpha : The angular velocity gain to rotate the robot towards the goal
28+
Kp_beta : The offset angular velocity gain accounting for smooth merging to
29+
the goal angle (i.e., it helps the robot heading to be parallel
30+
to the target angle.)
31+
"""
32+
33+
def __init__(self, Kp_rho, Kp_alpha, Kp_beta):
34+
self.Kp_rho = Kp_rho
35+
self.Kp_alpha = Kp_alpha
36+
self.Kp_beta = Kp_beta
37+
38+
def calc_control_command(self, x_diff, y_diff, theta, theta_goal):
39+
"""
40+
Returns the control command for the linear and angular velocities as
41+
well as the distance to goal
42+
43+
Parameters
44+
----------
45+
x_diff : The position of target with respect to current robot position
46+
in x direction
47+
y_diff : The position of target with respect to current robot position
48+
in y direction
49+
theta : The current heading angle of robot with respect to x axis
50+
theta_goal: The target angle of robot with respect to x axis
51+
52+
Returns
53+
-------
54+
rho : The distance between the robot and the goal position
55+
v : Command linear velocity
56+
w : Command angular velocity
57+
"""
58+
59+
# Description of local variables:
60+
# - alpha is the angle to the goal relative to the heading of the robot
61+
# - beta is the angle between the robot's position and the goal
62+
# position plus the goal angle
63+
# - Kp_rho*rho and Kp_alpha*alpha drive the robot along a line towards
64+
# the goal
65+
# - Kp_beta*beta rotates the line so that it is parallel to the goal
66+
# angle
67+
#
68+
# Note:
69+
# we restrict alpha and beta (angle differences) to the range
70+
# [-pi, pi] to prevent unstable behavior e.g. difference going
71+
# from 0 rad to 2*pi rad with slight turn
72+
73+
rho = np.hypot(x_diff, y_diff)
74+
alpha = (np.arctan2(y_diff, x_diff)
75+
- theta + np.pi) % (2 * np.pi) - np.pi
76+
beta = (theta_goal - theta - alpha + np.pi) % (2 * np.pi) - np.pi
77+
v = self.Kp_rho * rho
78+
w = self.Kp_alpha * alpha - controller.Kp_beta * beta
79+
80+
if alpha > np.pi / 2 or alpha < -np.pi / 2:
81+
v = -v
82+
83+
return rho, v, w
84+
85+
1686
# simulation parameters
17-
Kp_rho = 9
18-
Kp_alpha = 15
19-
Kp_beta = -3
87+
controller = PathFinderController(9, 15, 3)
2088
dt = 0.01
2189

2290
# Robot specifications
@@ -27,14 +95,6 @@
2795

2896

2997
def move_to_pose(x_start, y_start, theta_start, x_goal, y_goal, theta_goal):
30-
"""
31-
rho is the distance between the robot and the goal position
32-
alpha is the angle to the goal relative to the heading of the robot
33-
beta is the angle between the robot's position and the goal position plus the goal angle
34-
35-
Kp_rho*rho and Kp_alpha*alpha drive the robot along a line towards the goal
36-
Kp_beta*beta rotates the line so that it is parallel to the goal angle
37-
"""
3898
x = x_start
3999
y = y_start
40100
theta = theta_start
@@ -52,20 +112,8 @@ def move_to_pose(x_start, y_start, theta_start, x_goal, y_goal, theta_goal):
52112
x_diff = x_goal - x
53113
y_diff = y_goal - y
54114

55-
# Restrict alpha and beta (angle differences) to the range
56-
# [-pi, pi] to prevent unstable behavior e.g. difference going
57-
# from 0 rad to 2*pi rad with slight turn
58-
59-
rho = np.hypot(x_diff, y_diff)
60-
alpha = (np.arctan2(y_diff, x_diff)
61-
- theta + np.pi) % (2 * np.pi) - np.pi
62-
beta = (theta_goal - theta - alpha + np.pi) % (2 * np.pi) - np.pi
63-
64-
v = Kp_rho * rho
65-
w = Kp_alpha * alpha + Kp_beta * beta
66-
67-
if alpha > np.pi / 2 or alpha < -np.pi / 2:
68-
v = -v
115+
rho, v, w = controller.calc_control_command(
116+
x_diff, y_diff, theta, theta_goal)
69117

70118
if abs(v) > MAX_LINEAR_SPEED:
71119
v = np.sign(v) * MAX_LINEAR_SPEED
@@ -104,8 +152,9 @@ def plot_vehicle(x, y, theta, x_traj, y_traj): # pragma: no cover
104152
plt.plot(x_traj, y_traj, 'b--')
105153

106154
# for stopping simulation with the esc key.
107-
plt.gcf().canvas.mpl_connect('key_release_event',
108-
lambda event: [exit(0) if event.key == 'escape' else None])
155+
plt.gcf().canvas.mpl_connect(
156+
'key_release_event',
157+
lambda event: [exit(0) if event.key == 'escape' else None])
109158

110159
plt.xlim(0, 20)
111160
plt.ylim(0, 20)
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
"""
2+
3+
Move to specified pose (with Robot class)
4+
5+
Author: Daniel Ingram (daniel-s-ingram)
6+
Atsushi Sakai (@Atsushi_twi)
7+
Seied Muhammad Yazdian (@Muhammad-Yazdian)
8+
9+
P.I. Corke, "Robotics, Vision & Control", Springer 2017, ISBN 978-3-319-54413-7
10+
11+
"""
12+
13+
import matplotlib.pyplot as plt
14+
import numpy as np
15+
import copy
16+
from move_to_pose import PathFinderController
17+
18+
# Simulation parameters
19+
TIME_DURATION = 1000
20+
TIME_STEP = 0.01
21+
AT_TARGET_ACCEPTANCE_THRESHOLD = 0.01
22+
SHOW_ANIMATION = True
23+
PLOT_WINDOW_SIZE_X = 20
24+
PLOT_WINDOW_SIZE_Y = 20
25+
PLOT_FONT_SIZE = 8
26+
27+
simulation_running = True
28+
all_robots_are_at_target = False
29+
30+
31+
class Pose:
32+
"""2D pose"""
33+
34+
def __init__(self, x, y, theta):
35+
self.x = x
36+
self.y = y
37+
self.theta = theta
38+
39+
40+
class Robot:
41+
"""
42+
Constructs an instantiate of the 3-DOF wheeled Robot navigating on a
43+
2D plane
44+
45+
Parameters
46+
----------
47+
name : (string)
48+
The name of the robot
49+
color : (string)
50+
The color of the robot
51+
max_linear_speed : (float)
52+
The maximum linear speed that the robot can go
53+
max_angular_speed : (float)
54+
The maximum angular speed that the robot can rotate about its vertical
55+
axis
56+
path_finder_controller : (PathFinderController)
57+
A configurable controller to finds the path and calculates command
58+
linear and angular velocities.
59+
"""
60+
61+
def __init__(self, name, color, max_linear_speed, max_angular_speed,
62+
path_finder_controller):
63+
self.name = name
64+
self.color = color
65+
self.MAX_LINEAR_SPEED = max_linear_speed
66+
self.MAX_ANGULAR_SPEED = max_angular_speed
67+
self.path_finder_controller = path_finder_controller
68+
self.x_traj = []
69+
self.y_traj = []
70+
self.pose = Pose(0, 0, 0)
71+
self.pose_start = Pose(0, 0, 0)
72+
self.pose_target = Pose(0, 0, 0)
73+
self.is_at_target = False
74+
75+
def set_start_target_poses(self, pose_start, pose_target):
76+
"""
77+
Sets the start and target positions of the robot
78+
79+
Parameters
80+
----------
81+
pose_start : (Pose)
82+
Start postion of the robot (see the Pose class)
83+
pose_target : (Pose)
84+
Target postion of the robot (see the Pose class)
85+
"""
86+
self.pose_start = copy.copy(pose_start)
87+
self.pose_target = pose_target
88+
self.pose = pose_start
89+
90+
def move(self, dt):
91+
"""
92+
Moves the robot for one time step increment
93+
94+
Parameters
95+
----------
96+
dt : (float)
97+
time step
98+
"""
99+
self.x_traj.append(self.pose.x)
100+
self.y_traj.append(self.pose.y)
101+
102+
rho, linear_velocity, angular_velocity = \
103+
self.path_finder_controller.calc_control_command(
104+
self.pose_target.x - self.pose.x,
105+
self.pose_target.y - self.pose.y,
106+
self.pose.theta, self.pose_target.theta)
107+
108+
if rho < AT_TARGET_ACCEPTANCE_THRESHOLD:
109+
self.is_at_target = True
110+
111+
if abs(linear_velocity) > self.MAX_LINEAR_SPEED:
112+
linear_velocity = (np.sign(linear_velocity)
113+
* self.MAX_LINEAR_SPEED)
114+
115+
if abs(angular_velocity) > self.MAX_ANGULAR_SPEED:
116+
angular_velocity = (np.sign(angular_velocity)
117+
* self.MAX_ANGULAR_SPEED)
118+
119+
self.pose.theta = self.pose.theta + angular_velocity * dt
120+
self.pose.x = self.pose.x + linear_velocity * \
121+
np.cos(self.pose.theta) * dt
122+
self.pose.y = self.pose.y + linear_velocity * \
123+
np.sin(self.pose.theta) * dt
124+
125+
126+
def run_simulation(robots):
127+
"""Simulates all robots simultaneously"""
128+
global all_robots_are_at_target
129+
global simulation_running
130+
131+
robot_names = []
132+
for instance in robots:
133+
robot_names.append(instance.name)
134+
135+
time = 0
136+
while simulation_running and time < TIME_DURATION:
137+
time += TIME_STEP
138+
robots_are_at_target = []
139+
140+
for instance in robots:
141+
if not instance.is_at_target:
142+
instance.move(TIME_STEP)
143+
robots_are_at_target.append(instance.is_at_target)
144+
145+
if all(robots_are_at_target):
146+
simulation_running = False
147+
148+
if SHOW_ANIMATION:
149+
plt.cla()
150+
plt.xlim(0, PLOT_WINDOW_SIZE_X)
151+
plt.ylim(0, PLOT_WINDOW_SIZE_Y)
152+
153+
# for stopping simulation with the esc key.
154+
plt.gcf().canvas.mpl_connect(
155+
'key_release_event',
156+
lambda event: [exit(0) if event.key == 'escape' else None])
157+
158+
plt.text(0.3, PLOT_WINDOW_SIZE_Y - 1,
159+
'Time: {:.2f}'.format(time),
160+
fontsize=PLOT_FONT_SIZE)
161+
162+
plt.text(0.3, PLOT_WINDOW_SIZE_Y - 2,
163+
'Reached target: {} = '.format(robot_names)
164+
+ str(robots_are_at_target),
165+
fontsize=PLOT_FONT_SIZE)
166+
167+
for instance in robots:
168+
plt.arrow(instance.pose_start.x,
169+
instance.pose_start.y,
170+
np.cos(instance.pose_start.theta),
171+
np.sin(instance.pose_start.theta),
172+
color='r',
173+
width=0.1)
174+
plt.arrow(instance.pose_target.x,
175+
instance.pose_target.y,
176+
np.cos(instance.pose_target.theta),
177+
np.sin(instance.pose_target.theta),
178+
color='g',
179+
width=0.1)
180+
plot_vehicle(instance.pose.x,
181+
instance.pose.y,
182+
instance.pose.theta,
183+
instance.x_traj,
184+
instance.y_traj, instance.color)
185+
186+
plt.pause(TIME_STEP)
187+
188+
189+
def plot_vehicle(x, y, theta, x_traj, y_traj, color):
190+
# Corners of triangular vehicle when pointing to the right (0 radians)
191+
p1_i = np.array([0.5, 0, 1]).T
192+
p2_i = np.array([-0.5, 0.25, 1]).T
193+
p3_i = np.array([-0.5, -0.25, 1]).T
194+
195+
T = transformation_matrix(x, y, theta)
196+
p1 = T @ p1_i
197+
p2 = T @ p2_i
198+
p3 = T @ p3_i
199+
200+
plt.plot([p1[0], p2[0]], [p1[1], p2[1]], color+'-')
201+
plt.plot([p2[0], p3[0]], [p2[1], p3[1]], color+'-')
202+
plt.plot([p3[0], p1[0]], [p3[1], p1[1]], color+'-')
203+
204+
plt.plot(x_traj, y_traj, color+'--')
205+
206+
207+
def transformation_matrix(x, y, theta):
208+
return np.array([
209+
[np.cos(theta), -np.sin(theta), x],
210+
[np.sin(theta), np.cos(theta), y],
211+
[0, 0, 1]
212+
])
213+
214+
215+
def main():
216+
pose_target = Pose(15, 15, -1)
217+
218+
pose_start_1 = Pose(5, 2, 0)
219+
pose_start_2 = Pose(5, 2, 0)
220+
pose_start_3 = Pose(5, 2, 0)
221+
222+
controller_1 = PathFinderController(5, 8, 2)
223+
controller_2 = PathFinderController(5, 16, 4)
224+
controller_3 = PathFinderController(10, 25, 6)
225+
226+
robot_1 = Robot("Yellow Robot", "y", 12, 5, controller_1)
227+
robot_2 = Robot("Black Robot", "k", 16, 5, controller_2)
228+
robot_3 = Robot("Blue Robot", "b", 20, 5, controller_3)
229+
230+
robot_1.set_start_target_poses(pose_start_1, pose_target)
231+
robot_2.set_start_target_poses(pose_start_2, pose_target)
232+
robot_3.set_start_target_poses(pose_start_3, pose_target)
233+
234+
robots: list[Robot] = [robot_1, robot_2, robot_3]
235+
236+
run_simulation(robots)
237+
238+
239+
if __name__ == '__main__':
240+
main()

0 commit comments

Comments
 (0)