nobleo / path_tracking_pid Goto Github PK
View Code? Open in Web Editor NEWPath Tracking PID offers a tuneable PID control loop, decouling steering and forward velocity
License: Apache License 2.0
Path Tracking PID offers a tuneable PID control loop, decouling steering and forward velocity
License: Apache License 2.0
Steps to reproduce:
d_end_phase
(here) can result in negative values if target_x_vel
< 0
We append the global plan with the carrot distance here:
path_tracking_pid/src/controller.cpp
Lines 190 to 196 in 61c6cc8
But there are a few problems with this:
IMO, if the controller is incapable of handling the global plan, it should reject the global plan, not alter it to its liking. This filter_plan
call does exactly that. I think we should just check it, and return REJECTED
if it does not satisfy the controller's constraints to the global plan.
path_tracking_pid/src/controller.cpp
Lines 183 to 188 in 61c6cc8
Talking about this piece of code in setPlan()
, intended to avoid putting steps on the velocity setpoint:
path_tracking_pid/src/path_tracking_pid_local_planner.cpp
Lines 147 to 158 in 61c6cc8
Currently, the controller rejects any new goal when the error between actual and setpoint velocity is too large. It is unknown what happens then, because the controller will not send any velocities anymore. So this depends on what third party control action or disturbance made this happen.
IMO, we should accept this goal (as there is nothing wrong with the goal itself. The goal doesn't say anything about velocities). In computeVelocityCommands
, we should check that this error does not become too large. If that does happen, we should abort the goal, set the setpoint to the current odom and decrease that to zero, respecting our acceleration limits.
The behavior of the robot stays the same in the special case this was designed for: a manual override while controlling. Because if the actual velocity is zero, the error is too large, the goal is accepted, but it is aborted right away, controlling the velocity to zero (which it already is) and resetting the controller state.
But it should now also produce the desired behavior in all other cases: where the actual velocity is nonzero, but not similar to the controller state. The goal is accepted, but aborted due to a controller error. The velocity is smoothly decreased to zero, and the controller state is again in sync with the robot state.
geometry_msgs::Transform
tf_base_to_steered_wheel_geometry_msgs::Transform
that are used as arguments
setPlan
, move the conversion code to the ROS wrapperglobal_plan_
doesn't need to be a memberThe controller can be reconfigured while driving. If the controller is also reset, then the D and I action are reset to 0, and the lowpass starts from scratch. This is unwanted behavior.
path_tracking_pid/src/controller.cpp
Line 978 in ff653a6
Move base flex passes you the odometry at every call of computeVelocityCommands
, so there is no need for this subscriber:
The next piece of code checks for compatibility of the plan's frame and the frame the controller is running in. This makes assumptions about the application. Does the user want you to control in the plan's frame, or do they want the controller to convert the plan to the controller's frame? There's no way of knowing.
Keep the the controller (and the entire ROS system) clean by assuming you're running in the standardized frames (map
, odom
, or maybe utm
or something). In this case you're using the frame specified for move_base_flex
. If the user wants to be flexible with respect to the reference frames of navigation plans, they should transform any incoming plans to the move_base_flex
global frame in a separate rosnode with the isolated functionality of providing this flexible interface.
path_tracking_pid/src/path_tracking_pid_local_planner.cpp
Lines 96 to 117 in 00202b4
In the next piece of code, the cmd_vel
is set to the controller state:
path_tracking_pid/src/path_tracking_pid_local_planner.cpp
Lines 193 to 194 in 00202b4
path_tracking_pid/src/path_tracking_pid_local_planner.cpp
Lines 252 to 265 in 00202b4
When a cancel is requested, the controller changes its own parameters
path_tracking_pid/src/path_tracking_pid_local_planner.cpp
Lines 214 to 239 in 00202b4
Why not just set another limit just like in the obstacle handling (just above these lines).
This next bit only constructs some visualization markers to publish for debugging purposes, but it takes up a lot of space in the file:
path_tracking_pid/src/path_tracking_pid_local_planner.cpp
Lines 275 to 339 in 00202b4
A cancel request now blocks until the goal is no longer active (which will stay active until the robot has stopped).
path_tracking_pid/src/path_tracking_pid_local_planner.cpp
Lines 620 to 633 in 00202b4
true
immediately because you implemented a stopping behavior, like described here:This single function is close to 470 lines of code. (Makes you wonder if this is really just a PID controller...) We should at least split this up for readability:
path_tracking_pid/src/controller.cpp
Line 399 in 00202b4
To be continued...
Currently, closestPointOnSegment uses the angle of the start pose (or the one defined from start to end) to the returned Pose
A better idea is to interpolate the angle from start to end (when those angles are given). This will avoid "step" discontinuities of the returned Pose angle between segments, which I saw recently in one of our projects.
The controller as a child of nav_core::BaseLocalPlanner
has not been tested for a long time. It has been decided to drop this support (see #89 ), so we should remove the inheritance here:
path_tracking_pid/src/path_tracking_pid_local_planner.cpp
Lines 22 to 25 in 61c6cc8
Controller::mpc_based_max_vel()
repeatedly calls Controller::update()
but rolls back some of its side effects. This is a very convoluted and error-prone setup.
update()
sets the following member variables:
current_with_carrot_
current_pos_on_plan_
current_goal_
distance_to_goal_
by findPositionOnPlan()
- see #123controller_state_
current_target_x_vel_
Before mpc_based_max_vel()
calls update()
it creates a backup of controller_state_
and at certain points in its body (and just before the end) it overwrites the current state with this backup.
I propose the following:
mpc_based_max_vel()
should be a const function that does not modify any state. Based on the name and function documentation, this should be possible.update()
should be refactored/split in such a way that at the very least no side effects need to be rolled back in mpc_based_max_vel()
.Currently the filtered error is used in the P and D calculations but the raw error is used in the I calculation. All of them should use the filtered one.
In TrackingPidLocalPlanner::setPlan()
the passed global plan is transformed just once into the configured global_frame
.
If this latter is configured to be odom
, the global plan is sensitive to initial localisation faults.
Other planners (such as dwa_local_planner
) convert the inserted global plan every tick into global_frame
, using LocalPlannerUtil::getLocalPlan()
.
There is a number of variables that represent dimensions and quantities that are referenced in the explanation of the control strategy in the README. Naming of these variables is not consistent, though. Some variables are just unclear in what they represent from their names. This holds for (but is not limited to):
l
: the lookahead distance (not mentioned as such in the README yet)current_tf
current_x_vel
(which is the setpoint, I suppose)I put a step response on the second order lowpass and the output I've printed below. As you can see the filter has an overshoot, which is very strange. I think there is something wrong with it.
0.292893
0.87868
1.12132
1.02082
0.979185
0.996429
1.00357
1.00061
0.999387
0.999895
1.00011
1.00002
0.999982
0.999997
1
1
0.999999
1
1
1
A call to cancel
should return whether or not the controller implements stopping behavior. If false
, the base class handles it (and outputs a zero velocity setpoint right away). Meaning we should not block, but return true
immediately, in this case.
In a few places, there's magic numbers in the code. We may want to get rid of those. Some examples:
path_tracking_pid/include/path_tracking_pid/controller.hpp
Lines 22 to 24 in 1495813
path_tracking_pid/include/path_tracking_pid/controller.hpp
Lines 330 to 338 in 1495813
Currently the robot slows down based on values provided by the costmap2d. However, costmap2d fills its values based on a logic of robots with a (very close to) circular footprint robots. This makes that for rectangular robots, the robots would slow down a lot more when passing next to an obstacle than when it is approached with the corner. This can hinder the performance in environments with a lot of objects around like warehouses.
PR #25 changed this line:
path_tracking_pid/src/controller.cpp
Line 122 in b5c1391
@lewie-donckers I think this is quite dangerous. If pose_idx
is an unsigned int, that means that this comparison can underflow when global plan is empty.
Originally posted by @Rayman in #25 (comment)
This function breaks the abstraction and provides way too much detail to the caller. Only a few of its members are actually used by TrackingPidLocalPlanner
. The Controller
should provide dedicated getters with a higher level of abstraction and proper names.
This will be a lot easier once #123 has been done so the entire state structure does not need to be passed to that function.
dynamic reconfigure is an implementation of ROS which happens to use a large struct for its parameters.
However some of these we don't like to have dynamically reconfigurable.
I propose to have an own struct with the parameters. And let the path_tracking_pid::PidConfig config
be local to the reconfigure callback. In this callback we can (if approved) set the controller parameters with setters.
This might allow to switch to ddynamic_reconfigure (if we want to). Also allows for easier ROS2 transition in the future (it uses a different method for dynamic reconfigure behavior).
Thank for sharing u code. When I use full_coverage_path_planner and path_tracking_pid, I can not make it run. please help me. thanks.
this is the launch:
<!--Move base flex, using the full_coverage_path_planner-->
<node pkg="mbf_costmap_nav" type="mbf_costmap_nav" respawn="false" name="move_base_flex" output="screen" required="true">
<param name="tf_timeout" value="1.5"/>
<rosparam file="$(find full_coverage_path_planner)/test/full_coverage_path_planner/param/planners.yaml" command="load" />
<rosparam file="$(find full_coverage_path_planner)/test/full_coverage_path_planner/param/local_costmap_params.yaml" command="load" />
<rosparam file="$(find path_tracking_pid)/param/controllers.yaml" command="load" />
<rosparam file="$(find path_tracking_pid)/param/path_tracking_pid.yaml" command="load" />
<param name="SpiralSTC/robot_radius" value="$(arg robot_radius)"/>
<param name="SpiralSTC/tool_radius" value="$(arg tool_radius)"/>
<param name="global_costmap/robot_radius" value="$(arg robot_radius)"/>
<remap from="odom" to="/odom"/>
<remap from="scan" to="/scan"/>
<remap from="/move_base_flex/SpiralSTC/plan" to="/move_base/SpiralSTC/plan"/>
<!-- <remap from="/move_base_flex/PathTrackingPID/interpolator" to="/move_base/TrackingPidLocalPlanner/interpolator"/> -->
</node>
<!-- Move Base backwards compatibility -->
<node pkg="mbf_costmap_nav" type="move_base_legacy_relay.py" name="move_base" >
<param name="base_global_planner" value="SpiralSTC" />
<param name="base_local_planner" value="PathTrackingPID" />
</node>
<!-- Mobile robot simulator -->
<node pkg="mobile_robot_simulator" type="mobile_robot_simulator_node" name="mobile_robot_simulator" output="screen">
<param name="publish_map_transform" value="true"/>
<param name="publish_rate" value="10.0"/>
<param name="velocity_topic" value="/move_base/cmd_vel"/>
<param name="odometry_topic" value="/odom"/>
</node>
<!--We need a map to fully cover-->
<node name="grid_server" pkg="map_server" type="map_server" args="$(arg map)">
<param name="frame_id" value="map"/>
</node>
<!-- Launch coverage progress tracking -->
<node pkg="tf" type="static_transform_publisher" name="map_to_coveragemap" args="$(arg coverage_area_offset) map coverage_map 100" />
<node pkg="full_coverage_path_planner" type="coverage_progress" name="coverage_progress">
<param name="~target_area/x" value="$(arg coverage_area_size_x)" />
<param name="~target_area/y" value="$(arg coverage_area_size_y)" />
<param name="~coverage_radius" value="$(arg tool_radius)" />
<remap from="reset" to="coverage_progress/reset" />
<param name="~map_frame" value="/coverage_map"/>
</node>
<!-- Trigger planner by publishing a move_base goal -->
<node name="publish_simple_goal" pkg="rostopic" type="rostopic" launch-prefix="bash -c 'sleep 1.0; $0 $@' "
args="pub --latch /move_base/goal move_base_msgs/MoveBaseActionGoal --file=$(find full_coverage_path_planner)/test/simple_goal.yaml"/>
<!-- rviz -->
<node if="$(eval rviz)" name="rviz" pkg="rviz" type="rviz" args="-d $(find full_coverage_path_planner)/test/full_coverage_path_planner/fcpp.rviz" />
While working on unit tests for Controller::distToSegmentSquared()
I noticed the following.
The documentation suggests that the pose it returns is the point on the segment PV (start, end) that is closest to W (point). It does not do this.
I had the following test case:
TEST(PathTrackingPidCalculations, ClosestPointOnSegment_CloseToStart)
{
const auto start = create_transform(2, 2, 0);
const auto end = create_transform(4, 4, 0);
const auto point = create_transform(0, 1, 0);
const auto ref = start;
EXPECT_EQ(ref, closestPointOnSegment(start, end, point, false));
}
point
is closest to start
of any point on start
-end
but the following is returned: [1.759, 2.319, 0]
.
In determining the error, the controller assumes that the plan is "forward". I.e., the poses' x-axes are aligned with the direction of motion of the robot. It seems that the orientations are assumed to point towards the intended direction of motion. Using that assumption, you can use the poses on the plan to determine a path for the carrot to make the robot follow the plan well. To me, this seems like an abuse of the interface.
I've always interpreted these orientations as description of the robot's prescribed orientation at that position; not its direction of motion (although I can't find a strict definition of this). So the poses' may for example be in opposite direction, indicating that the robot should drive in reverse. Or they could be turned pi/2 in any direction, indicating that a (holonomic?) robot should traverse these points in a sideways motion, see the following video:
Using that definition, the poses may be oriented in any arbitrary direction. At multiple points in the code, this assumption throws a spanner in the works when sending goals that don't follow this assumption. Try sending a goal with interpolated poses (using ROS' default Global Planner for example)...
While refactoring Controller::findPositionOnPlan()
, I noticed the following. I'm not sure if it is intended or not so I would like some feedback. Depending on the feedback we can determine what to do (nothing, document, fix, etc).
In that function we look ahead in the plan based on a given index. As long as each next point is a better match, we keep looking and update the given index. Once we're done with looking ahead, we start looking backward. Now one of two things will happen:
So in practice we look either forward or backward but never both. Is this intended?
I've included the code below and stripped the bits that were less relevant:
// First look in current position and in front
for (auto idx_path = global_plan_index; idx_path < global_plan_tf_.size(); idx_path++) {
...
if (...) {
...
global_plan_index = idx_path;
} else {
break;
}
}
// Then look backwards
for (auto idx_path = global_plan_index; idx_path > 0; --idx_path) {
...
}
When cancelling the exe_path
action, the controller changes the configuration set by the user: it sets target_x_vel
to zero, and it never changes it back. As a result, the next goal sent on the exe_path
action interface is not executed well, because the target_x_vel
remains zero.
Original move_base
's nav_core::BaseLocalPlanner
is inherited from, but not properly supported.
This whole function only existed for the sake of the nav_core
implementation.
Now that we don't have that legacy anymore. I think there are much better improvements to be made.
No we're calling computeVelocityCommands
from the computeVelocityCommands
function. It doesn't add any semantics. The 'main' computeVelocityCommands
could be broken into functions with better descriptions.
Originally posted by @Timple in #115 (comment)
Maybe we could also move all of this to visualization.[h|c]pp
path_tracking_pid/src/path_tracking_pid_local_planner.cpp
Lines 295 to 359 in 1495813
The stopping distance in the end phase d_end_phase
(here) contains a component which accounts for the distance traveled during the possible delay due to having a sampling time.
However I wonder if it isn't too conservative, currently it is:
target_x_vel * 2 * dt
Whereas I think the average delay is 0.5*dt
, which would result in the following component:
target_x_vel * 0.5 * dt
Hi thanks for your work.
can you please give a example about how to use this package with global planner, I cannot understand how to subscribe topic "path". I readed the code , it do not subscribe any topic namde "path" or any topic with type of nav_msgs/Path.
thank you very much
Since there is no launch file nor test script, how can I run the path tracker?
I can build it successfully with colcon build.
Thanks!
A simple dot product would give the desired outcome: the length of the projection of one vector onto the other, with the sign of the cosine of the angle between the two vectors (which is what we're looking for).
path_tracking_pid/src/controller.cpp
Lines 581 to 593 in 61c6cc8
In TrackingPidLocalPlanner::projectedCollisionCost()
a local variable x_step_tf
is created but only it's origin component is initialized; it's basis component is left uninitialized.
Later this variable is used in computations which is undefined behavior.
This could be (one of) the reasons our tests sometimes fail.
@Timple
Hello,I put the path_tracking_pid package in the workspace for compilation, and found whether nav_core is registered, and the result is as follows:
At the same time, my launch file configuration is as follows:
But when I run it, the following error appears:
How can I solve this problem? Looking forward to your reply!
I think Controller::distToSegmentSquared()
should be refactored in several ways:
estimate_pose_angle_enabled_
. I propose we replace that with an input parameter.calculations.h/cpp
for example) and unit tests can be added.distSquared()
from the anonymous namespace in controller.cpp
would have to be moved too. I propose we do this and add unit tests for that as well.Controller::findPositionOnPlan()
should be refactored:
distance_to_goal_
. It is incorrect (or at least odd) to only change the distance but not any other state (current plan index, etc). I propose we make this function const and do not set any data members. It should just do as the name suggests and find something. If required this value can be made part of the return value. (But perhaps it might be easy to calculate by the caller.)path_pose_idx
is an output parameter. I propose we change it to be part of the return value.controller_state
contains way too much state. Only the following members are used:
last_visited_pose_index
is used as an output parameter. I propose we change it to be part of the return value.current_global_plan_index
is used as an in-/output parameter. I propose we split this into a pure input parameter and make the output part of the return value.Hello, I am using this package alone to follow the global path planned by myself, but the effect is very poor, mainly as follows:
controllers:
use_mpc: false
mpc_max_error_lat: 0.5
mpc_max_fwd_iter: 200
mpc_max_vel_optimization_iterations: 5
mpc_min_x_vel: 0.5
mpc_simulation_sample_time: 0.05
anti_collision: true
obstacle_speed_reduction: true
collision_look_ahead_resolution: 1.0 # [m]
global_frame_id: odom
map_frame: map
controller_frequency: 5.0
controller_max_retries: 0
Came across this corner case where angular field in the command velocity is set to nan
which may cause erratic movements on a robot if not handled on the receiving side. The nan
value is coming from feedforward_ang_
component of the controller, which uses inverse turning radius as:
path_tracking_pid/src/controller.cpp
Line 559 in 750f8c1
The part where inverse turn radii are computed have a return value of infinity when delta is below an epsilon specified which includes a zero value too :
path_tracking_pid/src/calculations.cpp
Lines 45 to 60 in 750f8c1
This is causing an instance of nan
command velocity (angular) in the controller which stays permanent even if inverse radii return back to finite values.
While working on unit tests for Controller::distToSegmentSquared()
I noticed that the z-component of the inputs is ignored in some calculations but not in all.
I doubt that this is intended; it looks like a bug but perhaps someone can shed some light on this:
const double l2 = distSquared(pose_v, pose_w); // z used for l2
if (l2 == 0) {
return pose_w; // <-- z included in result in this case. not in the other return
}
tf2::Transform result;
const double t = std::clamp(
((pose_p.getOrigin().x() - pose_v.getOrigin().x()) *
(pose_w.getOrigin().x() - pose_v.getOrigin().x()) +
(pose_p.getOrigin().y() - pose_v.getOrigin().y()) *
(pose_w.getOrigin().y() - pose_v.getOrigin().y())) /
l2,
0.0, 1.0); // <-- z used in l2 but not in rest of calculation of t. which in turn is used for x and y of result.
result.setOrigin(tf2::Vector3(
pose_v.getOrigin().x() + t * (pose_w.getOrigin().x() - pose_v.getOrigin().x()),
pose_v.getOrigin().y() + t * (pose_w.getOrigin().y() - pose_v.getOrigin().y()), 0.0)); // <-- z forced to 0 in result
const auto yaw = estimate_pose_angle ? atan2(
pose_w.getOrigin().y() - pose_v.getOrigin().y(),
pose_w.getOrigin().x() - pose_v.getOrigin().x())
: tf2::getYaw(pose_v.getRotation());
tf2::Quaternion pose_quaternion;
pose_quaternion.setRPY(0.0, 0.0, yaw);
result.setRotation(pose_quaternion);
return result;
After a discussion with @cesar-lopez-mar:
In the figure below,
The controller checks if there is any cell within the collision polygon that is of value costmap2d::INSCRIBED_INFLATED_OBASTACLE
. If there is, it concludes that a collision is imminent. This is not true. the robot footprint is perfectly allowed to enter space of costmap2d::INSCRIBED_INFLATED_OBASTACLE
value. Here's an example where there is overlap, but no collision:
The cheap way to check for imminent collisions, assuming a (approximately) circular base is to check if the cell in which (a predicted) base_link
is located is >= costmap2d::INSCRIBED_INFLATED_OBASTACLE
. That would mean a guaranteed collision.
However, with a non-circular base, this is not enough. Because a different orientation may also lead to collision before base_link
reaches this space. Instead, you can check if any of the cells in the collision polygon exceeds this value. That means it reaches a value of >= costmap2d::LETHAL_OBSTACLE
.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.