Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Simultaneous execution of trajectories #2810

Closed
wants to merge 18 commits into from

Conversation

cambel
Copy link
Contributor

@cambel cambel commented Aug 13, 2021

@felixvd and I have been working on this PR

Description

This draft PR proposes a method to allow the execution of multiple controllers in several independent trajectory plans at the same time.
So far, MoveIt only supports the execution of one trajectory at a time. If a second trajectory is sent for execution, the first trajectory is aborted and the second one is attempted instead.

There are two main reasons for this limitation. First, all the action servers are of the type SimpleActionServer which can only serve one goal at a time and immediately reject any old goal as a new one is received. Second, the TrajectoryExecutionManager only supports the execution of one trajectory at a time by default, aborting any previous trajectory as a new one is pushed.

The first issue is addressed by converting the SimpleActionServers into (General) ActionServers. This allows the server to handle multiple goals at the same time.
The second issue is addressed by implementing a scheduling procedure to execute trajectories simultaneously as long as the following conditions are met:

  • The controllers needed for executing the trajectory are not being used.
  • The trajectory is not in collision with any active trajectory.
  • The trajectory is not in collision with the current state. This is assumed in the current trajectory execution scheme as it is expected that the planning covers this validation. For most cases, it works fine, but for simultaneous execution of trajectories, that assumption does not hold anymore, so an extra validation is needed.
  • The start state of the trajectory matched that of the current state of the robot.

If the trajectory does not meet the conditions mentioned above, the trajectory is stored in a backlog and periodically checked. The trajectories in the backlog and newly received trajectories are evaluated and executed in a strict FIFO order.

Our use case: Simultaneous control of robots with different sets of instructions that do not have/need to be synchronized in any particular way. See this simple equip/unequip routine where both robots can follow their independent set of instructions without waiting for the other one.

equip.unequip.tool.mp4

There are some issues pending to be fixed:

  • Re-enable feedback for the action servers.
  • Simultaneous planning seems to crash MoveIt/move_group. It is a non-deterministic bug that we are still trying to understand. I am trying to debug here. The problem is not uncommon, it happens during the planning, as we do plan_only and then execute the plans. It even happens with only one robot planning at a time. The crash happens when sending a solution plan to the goal_handle (of the ActionServer), for whatever reason the executed trajectory of the action response (MoveGroupResult) is corrupted because something cannot be accessed Cannot access memory at address.
  • Unless I am mistaken, the execute_trajectory_service also needs to be changed over to simultaneous execution
  • There are no tests for the new behavior yet

Comments and contributions are highly appreciated!

Checklist

  • Required by CI: Code is auto formatted using clang-format
  • Extend the tutorials / documentation reference
  • Document API changes relevant to the user in the MIGRATION.md notes
  • Create tests, which fail without this PR reference
  • Include a screenshot if changing a GUI
  • While waiting for someone to review your request, please help review another open pull request to support the maintainers

@welcome
Copy link

welcome bot commented Aug 13, 2021

Thanks for helping in improving MoveIt and open source robotics!

@cambel cambel changed the title Simultaneous motions Draft: Simultaneous execution of trajectories Aug 13, 2021
@felixvd
Copy link
Contributor

felixvd commented Aug 13, 2021

Fixes #2287

@JafarAbdi @henningkayser @tylerjw This is the project I mentioned in the call yesterday and the maintainer meeting. Some issues remain, but we have been running this for a few weeks and it should be ready for testing and feedback from a fresh set of eyes. There are infrequent crashes, probably related to thread safety in move_action.cpp. It would be great if someone could have a look.

Some more notes:

  • We renamed the functions and variables in TrajectoryExecutionManager to specify their behavior (blocking vs simultaneous).
  • The class originally had two quasi-redundant behaviors. The logic of the blocking queue could probably be removed now, but there are some calls in plan_execution.cpp and plan_with_sensing.cpp and I am not too familiar with those files. I'd like a second opinion. @v4hn ?
  • We added the group_name to RobotTrajectory because we need to know it for tracking the active joints in the TEM class.

Since the video above does not do this PR justice (slow PlanningScene update rate and ugly CollisionObjects 💩), I want to add some more to drive home the point how sweet and deserving of enthusiastic reviews this is:

  1. Two arms rearranging an objects in their own part of the workspace independently, each planning in their own thread in peace, simultaneously, without a care in the world
simultaneous-rearrange.mp4
  1. The right arm aligns the screw holes of the bearing housing (using vision) while the left arm picks the tool and a screw from the feeder in the back
bearing-align.mp4
  1. The right arm inserts the shaft while the left arm picks screws and fastens the bearing. Note how close the robots pass each other and how suicidal this would be without the TrajectoryExecutionMonitor ensuring that the trajectories don't collide.
shaft-insert-3.mp4

Anyway, I hope we can get the kinks ironed out soon. I think this PR is in everyone's interest. Thanks!

PS: I originally thought that the collision check between two trajectories could be done more efficiently with gpu-voxels, so we wasted a bit of time on trying that. That was absolutely premature optimization though, the performance as is seems perfectly fine.

Copy link
Member

@henningkayser henningkayser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cambel @felixvd Whoa, this is a lot. Great work so far! I really like the general idea of having an ongoing loop in the TEM that keeps track of any active trajectories and controllers. By validating incoming trajectories with all active instances in the beginning it should be possible to ensure we are at least commanding collision-free trajectories (fingers crossed that the controllers are actually doing what we command ;)). I haven't fully understood all of the new logic yet, I'll follow up with more in-depth and structural feedback. I also still need to make up my mind about how concurrent actions could be handled in general.

So far, my biggest concern is that collision checking between trajectories is inherently unsafe if we do it the way it's implemented right now. Self collisions have reduced padding by default and the resolution of trajectory waypoints is usually not sufficient to really guarantee that different groups don't run into each other.
As already mentioned in one of the comments, I think that we need to:

  • Increase padding for self collisions (= collisions between different groups)
  • Ensure a minimal joint distance resolution similar to longest_valid_fragment_section
  • Use a "continuous" collision check that validates the full segment between two consecutive waypoints. I'm not sure if this is currently even supported for self-collisions, but I imagine that this would be the most secure and efficient solution long-term. This could make the minimal joint distance threshold redundant.

Using a very big padding with a reasonable trajectory resolution should be fine for most use cases, but you could still not really rule out collisions for all cases as small joint state jumps can produce larger Cartesian distances.

Do you have a testing setup that you could share for testing?

@@ -231,6 +231,7 @@ void RobotTrajectory::getRobotTrajectoryMsg(moveit_msgs::RobotTrajectory& trajec

std::vector<const moveit::core::JointModel*> onedof;
std::vector<const moveit::core::JointModel*> mdof;
trajectory.group_name = group_ ? group_->getName() : "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you open a PR to add group_name to the message?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moveit/moveit_msgs#133 for reference

root_node_handle_, "sequence_move_group",
boost::bind(&MoveGroupSequenceAction::executeSequenceCallback, this, _1), false);
move_action_server_->registerPreemptCallback(boost::bind(&MoveGroupSequenceAction::preemptMoveCallback, this));
// move_action_server_->registerPreemptCallback(boost::bind(&MoveGroupSequenceAction::preemptMoveCallback, this));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use registerCancelCallback()?

move_state_ = state;
move_feedback_.state = stateToStr(state);
move_action_server_->publishFeedback(move_feedback_);
// move_state_ = state;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need to keep track of states for each goal separately and pass the goal handle into this function for publishing feedback

@@ -60,6 +60,8 @@ class BaseFakeController : public moveit_controller_manager::MoveItControllerHan
protected:
std::vector<std::string> joints_;
const ros::Publisher& pub_;
moveit_controller_manager::ExecutionStatus status_;
std::string name_ = "fake_controllers";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better set the default with the constructor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if this is only used for the logger names, I'd prefer adding a constexpr char LOGNAME[] to the source file instead of a member. See code style guidelines

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay

else
move_action_server_->setAborted(action_res, response);
}
goal_handle.setAborted(action_res, response);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here, setCanceled() could replace setPreempted()

TrajectoryExecutionContext* priority_context = it2->first; // Previous context in the backlog (earlier ones have priority)
ROS_DEBUG_STREAM_NAMED(name_, "Backlog comparing item with duration: " << current_context->trajectory_parts_[0].joint_trajectory.points.back().time_from_start);
ROS_DEBUG_STREAM_NAMED(name_, "vs item with duration: " << priority_context->trajectory_parts_[0].joint_trajectory.points.back().time_from_start);
if (hasCommonHandles(*current_context, *priority_context))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really a useful debug comment like this? It would help if the blocked resources (group, controller) were referenced here.

}
if(controllers_not_used_earlier_in_backlog)
{
ROS_DEBUG_STREAM_NAMED(name_, "Backlog item with duration " << current_context->trajectory_parts_[0].joint_trajectory.points.back().time_from_start << " will be checked and pushed to controller.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

time_from_start is used a couple times in this block.
Using const auto& time_from_start = current_context->trajectory_parts_[0].joint_trajectory.points.back().time_from_start would make this function more readable imo

std::deque<std::pair<TrajectoryExecutionContext*, ros::Time>> backlog;
int expiration_time = 60; // seconds (after this time, the trajectory is discarded) TODO (cambel): Make this less surprising

ros::Rate r(10);
while (run_continuous_execution_thread_)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop seems a little too convoluted to me. I'll take some more time to review this more in depth and see if this could be simplified somehow.

if (jointTrajPointToRobotState(active_trajectory.joint_trajectory, i, start_state))
{
robotStateToRobotStateMsg(start_state, start_state_msg);
if(!ps->isPathValid(start_state_msg, new_trajectory, new_trajectory.group_name, true))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty risky. isPathValid() doesn't do any interpolation between the waypoints and for the most part the resolution of the planned and parameterized trajectories is just not high enough to guarantee validity. I think we must consider the following three things:

  • Increase padding for self collisions (= collisions between different groups)
  • Ensure a minimal joint distance resolution similar to longest_valid_fragment_section
  • Use a "continuous" collision check that validates the full segment between two consecutive waypoints. I'm not sure if this is currently even supported for self-collisions, but I imagine that this would be the most secure and efficient solution long-term.

{
moveit_msgs::RobotState robot_state_msg;
robotStateToRobotStateMsg(*current_state, robot_state_msg);
if(!ps->isPathValid(robot_state_msg, trajectory, trajectory.group_name))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same story here

@henningkayser
Copy link
Member

Just a thought, it might make sense to split this PR into two parts:

  1. Update TrajectoryExecutionManager to support simultaneous trajectories + test directly using MoveItCpp
  2. Modify capabilities to support this new feature

@cambel cambel force-pushed the simultaneous-motions branch from 1ea23fc to 067dded Compare September 20, 2021 03:35
@cambel
Copy link
Contributor Author

cambel commented Sep 20, 2021

@henningkayser Thanks for the comments. I will slowly check them and update the PR. I created a simple environment to test this PR here using the Panda dual-arm env.

@felixvd
Copy link
Contributor

felixvd commented Oct 1, 2021

You can now also test this PR using the repository we originally wrote it for.

@simonschmeisser
Copy link
Contributor

I had a quick look at the current state of this PR as well and I think we should factor out at least two additional PRs:

  1. changing name_ to LOGNAME
  2. add a PlanningScene::isPathValid variant that accepts some maximum discretization parameter and subsamples in between trajectory points. If a collision checker supports continuous collision checking more efficiently, it could still use that internally if desired.
  3. alternatively we could also provide a separate subsampling algorithm and use the existing isPathValid

Then we need to interpolate the current trajectory and check each point with the new isPathValid

(repeating myself 😉 , but please check out pre-commit https://moveit.ros.org/documentation/contributing/code/ )

@cambel cambel force-pushed the simultaneous-motions branch from 067dded to 8958ef8 Compare June 19, 2022 04:14
Copy link
Contributor

@tahsinkose tahsinkose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, interesting PR. I have the following questions in mind:

  1. The goal of this PR seems to be the non-unary manipulation systems, which are represented through a single robot model possibly with different groups. Is this correct?
  2. Assuming the answer to 1 is "yes", how is this method better than solving the coupled planning problem - i.e both arms(or all active joints)- at once?

// This is called when the trajectory finishes.
bool execution_complete = false;
auto goalId = goal.getGoalID();
// Copy goalId here so that it is still available when this lambda is called later (otherwise it is deleted when the parent function ends)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, there shouldn't be any by-reference captures because they would suffer due to the same scoping problem, so you shouldn't need & before goalId 👀

// This is called when the trajectory finishes.
auto goalId = goal.getGoalID();
// Copy goalId here so that it is still available when this lambda is called later (otherwise it is deleted when the parent function ends)
auto completedTrajectoryCallback = [&, goalId](const moveit_controller_manager::ExecutionStatus& status) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a member function. It is too large for a lambda function

action_res.error_code.val = moveit_msgs::MoveItErrorCodes::PREEMPTED;
return;
}
// TODO(cambel): add a way to cancel a planning request
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? Are you going to also provide the simultaneous planning capability?

Comment on lines +90 to +92
moveit_msgs::RobotTrajectory trajectory_;

ExecutionCompleteCallback execution_complete_callback;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit -> docs

Update loggings


Apply clang format
@cambel cambel force-pushed the simultaneous-motions branch from 720b66a to 04cec9b Compare July 4, 2022 04:55
@cambel
Copy link
Contributor Author

cambel commented Jul 4, 2022

Hi @tahsinkose,

  1. This PR is for non-unary manipulation systems, however, there is no specific limitation introduced here about how the robots should be represented. However, representing the robots as a single robot model with different joint groups was the approach used to test this feature.

how is this method better than solving the coupled planning problem

  1. That is a good question. With a coupled planning system, you can only solve planning problems where each robot knows what the others are going to do (how they are going to move). That approach is useful and enough for some applications. However, for more complex applications where you want each robot to solve a different independent task, knowing what the other robots will do beforehand is not possible. So, controlling each joint group independently but simultaneously is necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants