Skip to content

Commit b697c82

Browse files
authored
Limit diagonal speed in path planning (#534)
2 parents 2f27f79 + 7bf46fe commit b697c82

File tree

4 files changed

+62
-43
lines changed

4 files changed

+62
-43
lines changed

bitbots_navigation/bitbots_path_planning/bitbots_path_planning/controller.py

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -108,35 +108,40 @@ def step(self, path: Path) -> tuple[Twist, PointStamped]:
108108
self.node.config.controller.max_rotation_vel,
109109
)
110110

111+
# Calculate the heading of our linear velocity vector in the local frame of the robot
112+
local_heading = walk_angle - self._get_yaw(current_pose)
113+
114+
# Convert the heading of the robot to a unit vector
115+
local_heading_vector_x = math.cos(local_heading)
116+
local_heading_vector_y = math.sin(local_heading)
117+
118+
# Returns interpolates the x and y maximum velocity linearly based on our heading
119+
# See https://github.com/bit-bots/bitbots_main/issues/366#issuecomment-2161460917
120+
def interpolate_max_vel(x_limit, y_limit, target_x, target_y):
121+
n = (x_limit * abs(target_y)) + y_limit * target_x
122+
intersection_x = (x_limit * y_limit * target_x) / n
123+
intersection_y = (x_limit * y_limit * abs(target_y)) / n
124+
return math.hypot(intersection_x, intersection_y)
125+
126+
# Limit the x and y velocity with the heading specific maximum velocity
127+
walk_vel = min(
128+
walk_vel,
129+
# Calc the max velocity for the current heading
130+
interpolate_max_vel(
131+
# Use different maximum values for forward and backwards x movement
132+
self.node.config.controller.max_vel_x
133+
if local_heading_vector_x > 0
134+
else self.node.config.controller.min_vel_x,
135+
self.node.config.controller.max_vel_y,
136+
local_heading_vector_x,
137+
local_heading_vector_y,
138+
),
139+
)
140+
111141
# Calculate the x and y components of our linear velocity based on the desired heading and the
112142
# desired translational velocity.
113-
cmd_vel.linear.x = math.cos(walk_angle - self._get_yaw(current_pose)) * walk_vel
114-
cmd_vel.linear.y = math.sin(walk_angle - self._get_yaw(current_pose)) * walk_vel
115-
116-
# Scale command accordingly if a limit is exceeded
117-
if cmd_vel.linear.x > self.node.config.controller.max_vel_x:
118-
self.node.get_logger().debug(
119-
f"X max LIMIT reached: {cmd_vel.linear.x} > {self.node.config.controller.max_vel_x}, with Y being {cmd_vel.linear.y}"
120-
)
121-
cmd_vel.linear.y *= self.node.config.controller.max_vel_x / cmd_vel.linear.x
122-
cmd_vel.linear.x = self.node.config.controller.max_vel_x
123-
self.node.get_logger().debug(f"X max LIMIT reached: scale Y to {cmd_vel.linear.y}")
124-
125-
if cmd_vel.linear.x < self.node.config.controller.min_vel_x:
126-
self.node.get_logger().debug(
127-
f"X min LIMIT reached: {cmd_vel.linear.x} < {self.node.config.controller.min_vel_x}, with Y being {cmd_vel.linear.y}"
128-
)
129-
cmd_vel.linear.y *= self.node.config.controller.min_vel_x / cmd_vel.linear.x
130-
cmd_vel.linear.x = self.node.config.controller.min_vel_x
131-
self.node.get_logger().debug(f"X min LIMIT reached: scale Y to {cmd_vel.linear.y}")
132-
133-
if abs(cmd_vel.linear.y) > self.node.config.controller.max_vel_y:
134-
self.node.get_logger().debug(
135-
f"Y max LIMIT reached: {cmd_vel.linear.y} > {self.node.config.controller.max_vel_y}, with X being {cmd_vel.linear.x}"
136-
)
137-
cmd_vel.linear.x *= self.node.config.controller.max_vel_y / abs(cmd_vel.linear.y)
138-
cmd_vel.linear.y = math.copysign(self.node.config.controller.max_vel_y, cmd_vel.linear.y)
139-
self.node.get_logger().debug(f"Y max LIMIT reached: scale X to {cmd_vel.linear.x}")
143+
cmd_vel.linear.x = local_heading_vector_x * walk_vel
144+
cmd_vel.linear.y = local_heading_vector_y * walk_vel
140145

141146
# Apply the desired rotational velocity
142147
cmd_vel.angular.z = rot_goal_vel

bitbots_navigation/bitbots_path_planning/config/path_planning_parameters.yaml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
bitbots_path_planning:
2-
base_footprint_frame:
2+
base_footprint_frame:
33
type: string
44
default_value: base_footprint
55
description: 'The frame of the robot base'
@@ -16,7 +16,7 @@ bitbots_path_planning:
1616
description: 'The rate at which the path planning is executed'
1717
validation:
1818
bounds<>: [0.0, 100.0]
19-
19+
2020
map:
2121
planning_frame:
2222
type: string
@@ -86,7 +86,7 @@ bitbots_path_planning:
8686

8787
max_vel_x:
8888
type: double
89-
default_value: 0.15
89+
default_value: 0.12
9090
description: 'Maximum velocity we want to reach in different directions (base_footprint coordinate system)'
9191
validation:
9292
bounds<>: [0.0, 1.0]
@@ -132,4 +132,3 @@ bitbots_path_planning:
132132
description: 'Distance at which we switch from orienting towards the path to orienting towards the goal poses orientation (in meters)'
133133
validation:
134134
bounds<>: [0.0, 10.0]
135-

bitbots_navigation/bitbots_path_planning/test/__snapshots__/test_controller.ambr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
dict({
44
'carrot_distance': 20,
55
'max_rotation_vel': 0.4,
6-
'max_vel_x': 0.15,
6+
'max_vel_x': 0.12,
77
'max_vel_y': 0.1,
88
'min_vel_x': -0.1,
99
'orient_to_goal_distance': 1.0,
@@ -14,5 +14,5 @@
1414
})
1515
# ---
1616
# name: test_step_cmd_vel_smoothing
17-
'geometry_msgs.msg.Twist(linear=geometry_msgs.msg.Vector3(x=0.15, y=0.09999999999999996, z=0.0), angular=geometry_msgs.msg.Vector3(x=0.0, y=0.0, z=0.20666164469042259))'
17+
'geometry_msgs.msg.Twist(linear=geometry_msgs.msg.Vector3(x=0.073884548439286, y=0.051963071290922924, z=0.0), angular=geometry_msgs.msg.Vector3(x=0.0, y=0.0, z=0.20666164469042259))'
1818
# ---

bitbots_navigation/bitbots_path_planning/test/test_controller.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import math
12
from unittest.mock import Mock
23

34
import pytest
@@ -37,7 +38,7 @@ def test_step_limits_forward_x_velocity(node, tf2_buffer, config, pose_opponent_
3738

3839
controller.step(path_to(pose_opponent_goal))
3940

40-
assert controller.last_cmd_vel.linear.x == config.controller.max_vel_x
41+
assert controller.last_cmd_vel.linear.x == pytest.approx(config.controller.max_vel_x)
4142
assert controller.last_cmd_vel.linear.y == pytest.approx(0)
4243
assert controller.last_cmd_vel.linear.z == 0
4344

@@ -47,7 +48,7 @@ def test_step_limits_backward_x_velocity(node, tf2_buffer, config, pose_own_goal
4748

4849
controller.step(path_to(pose_own_goal))
4950

50-
assert controller.last_cmd_vel.linear.x == config.controller.min_vel_x
51+
assert controller.last_cmd_vel.linear.x == pytest.approx(config.controller.min_vel_x)
5152
assert controller.last_cmd_vel.linear.y == pytest.approx(0)
5253
assert controller.last_cmd_vel.linear.z == 0
5354

@@ -58,7 +59,7 @@ def test_step_limits_forward_y_velocity(node, tf2_buffer, config, pose_left_line
5859
controller.step(path_to(pose_left_line))
5960

6061
assert controller.last_cmd_vel.linear.x == pytest.approx(0)
61-
assert controller.last_cmd_vel.linear.y == config.controller.max_vel_y
62+
assert controller.last_cmd_vel.linear.y == pytest.approx(config.controller.max_vel_y)
6263
assert controller.last_cmd_vel.linear.z == 0
6364

6465

@@ -68,7 +69,7 @@ def test_step_limits_backward_y_velocity(node, tf2_buffer, config, pose_right_li
6869
controller.step(path_to(pose_right_line))
6970

7071
assert controller.last_cmd_vel.linear.x == pytest.approx(0)
71-
assert controller.last_cmd_vel.linear.y == -config.controller.max_vel_y
72+
assert controller.last_cmd_vel.linear.y == pytest.approx(-config.controller.max_vel_y)
7273
assert controller.last_cmd_vel.linear.z == 0
7374

7475

@@ -77,8 +78,15 @@ def test_step_limits_forward_xy_velocities(node, tf2_buffer, config, pose_oppone
7778

7879
controller.step(path_to(pose_opponent_corner))
7980

80-
assert controller.last_cmd_vel.linear.x == pytest.approx(config.controller.max_vel_x)
81-
assert controller.last_cmd_vel.linear.y == pytest.approx(config.controller.max_vel_y)
81+
goal_heading_angle = math.atan2(pose_opponent_corner.pose.position.y, pose_opponent_corner.pose.position.x)
82+
walk_command_angle = math.atan2(controller.last_cmd_vel.linear.y, controller.last_cmd_vel.linear.x)
83+
84+
assert goal_heading_angle == pytest.approx(walk_command_angle)
85+
86+
walk_command_speed = math.hypot(controller.last_cmd_vel.linear.x, controller.last_cmd_vel.linear.y)
87+
88+
assert walk_command_speed < config.controller.max_vel_x
89+
assert walk_command_speed < config.controller.max_vel_y
8290
assert controller.last_cmd_vel.linear.z == 0
8391

8492

@@ -87,20 +95,27 @@ def test_step_limits_backward_xy_velocities(node, tf2_buffer, config, pose_own_c
8795

8896
controller.step(path_to(pose_own_corner))
8997

90-
assert controller.last_cmd_vel.linear.x == pytest.approx(config.controller.min_vel_x)
91-
assert controller.last_cmd_vel.linear.y == pytest.approx(-0.066666666)
98+
goal_heading_angle = math.atan2(pose_own_corner.pose.position.y, pose_own_corner.pose.position.x)
99+
walk_command_angle = math.atan2(controller.last_cmd_vel.linear.y, controller.last_cmd_vel.linear.x)
100+
101+
assert goal_heading_angle == pytest.approx(walk_command_angle)
102+
103+
walk_command_speed = math.hypot(controller.last_cmd_vel.linear.x, controller.last_cmd_vel.linear.y)
104+
105+
assert walk_command_speed < abs(config.controller.min_vel_x)
106+
assert walk_command_speed < config.controller.max_vel_y
92107
assert controller.last_cmd_vel.linear.z == 0
93108

94109

95110
def test_step_limits_rotation(node, tf2_buffer, config, pose_left_line, pose_right_line):
96111
controller = setup_controller(node, tf2_buffer)
97112

98113
controller.step(path_to(pose_left_line))
99-
assert controller.last_cmd_vel.angular.z == config.controller.max_rotation_vel
114+
assert controller.last_cmd_vel.angular.z == pytest.approx(config.controller.max_rotation_vel)
100115

101116
controller.last_update_time = None
102117
controller.step(path_to(pose_right_line))
103-
assert controller.last_cmd_vel.angular.z == -config.controller.max_rotation_vel
118+
assert controller.last_cmd_vel.angular.z == pytest.approx(-config.controller.max_rotation_vel)
104119

105120

106121
def test_step_cmd_vel_smoothing(snapshot, node, tf2_buffer, config, pose_opponent_corner):

0 commit comments

Comments
 (0)