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

TrajectoryController: Use desired state to calculate hold trajectory #297

Merged
merged 5 commits into from
Mar 16, 2018

Conversation

agutenkunst
Copy link
Contributor

The current state is used for calculating the hold-trajectory, that results in a jump in the desired position. The time delay originating from the canopen communication leads to a desired position that is already in the past.

Below the effect can be clearly seen by comparing the trajectories (plotted from the CAN traces).

kinetic-vs-fix_06

It can be clearly seen that the desired trajectory switches the sign of the velocity. This results in a jerk of the respective drive. If the speed is high and/or the stop_time is reduced this effect increases.
I would also argue that this behavior causes mechanical stress in the robot.

Any opinions?

Noticed with Schunk LWA4p

@bmagyar
Copy link
Member

bmagyar commented Sep 26, 2017

By looking at the plot and some experience with robots using this I think you are onto something.

We do have however a policy to add tests to every issue we encounter and then the fix for it. This makes sure that a newly discovered problem will stay fixed.

Could you please reproduce the issue within a test? Without your fix the test should fail. You can measure the jerk you mentioned.

@mathias-luedtke
Copy link
Contributor

mathias-luedtke commented Sep 27, 2017

I can confirm these jump movements in certain cases with the Schunk arms, but I haven't found out why.

In ros_canopen the state should be 2 cycles (-> 20ms) ahead of the command.
(command(t) gets sent at the same time as state(t+1) gets requested, the goal should be reached at state(t+2).)

I think the fix is correct. The last command should be kept in order to prevent jumps.

To further understand the effect and to provide a proper test:
What raw data are you plotting (and how)?
Can you plot as data points (e.g. small circles) or provide a bag file?
What's the input of the controller?
What's in the state topic (the CAN data might be affected by the limit interfaces)?

@jschleicher
Copy link
Contributor

I've added a testcase. Please Review and comment on the code / test.

New Plots from the testcase run with
roslaunch joint_trajectory_controller joint_trajectory_controller.test display_plots:=True rosrun joint_trajectory_controller joint_trajectory_controller_test

testcase_jerk_on_stop_pos
testcase_jerk_on_stop_vel

This PR is the same as #176 and #177; we only searched the issues and thus opened a duplicate PR, sorry for that.

@mathias-luedtke
Copy link
Contributor

Reviewing the old discussions using the desired state might lead to problems as well.
This is even worse because setHoldPosition gets called if the tracking error is too high.

As a compromise the hold target could get capped by the max. allowed tracking error.
Not sure if this really makes sense..

I've added a testcase. Please Review and comment on the code / test.

The test looks good, but I haven't tried it yet.

The delay topic just enables or disables the delay, so this should get reflected in a std_msgs/Bool topic or a std_srvs//SetBoolservice.
As an alternative the UInt8 value could specify the delay (= buffer size).

@jschleicher
Copy link
Contributor

@ipa-mdl I'm not aware of all use-cases, where ros_controllers is used. But sending out desired values to an underlying HW-interface (can be PID or any other control), I don't see any point in jumping from a desired position with error to the actual position (what is the step response of the underlying controller, if the desired position changes abruptly?).

The main point of this PR is, that while a joint is moving, the actual position has a delay and is thus behind the first point of the calculated stop trajectory. A jump backwards cannot be the desired behaviour.

Adolfo-rt suggests in a comment, to preserve the old behaviour, if stop_time is zero. For me that could be an appropriate compromise. Please give me an example, where that is needed.

See pull request for fix/jerk_on_stop for detailed description.

This commit adds a delay subscriber to rrbot to emulate a hardware controller with dead time.
The jump backwards in the actual position is observed in joint_trajectory_controller_test by
extending the stateCB.
The new testCase fails, if the velocity of the actual position is negative although motion should only be forwards.
@jschleicher
Copy link
Contributor

I've changed the test case accordingly.

The delay topic just enables or disables the delay, so this should get reflected in a std_msgs/Bool topic or a std_srvs//SetBoolservice.

@jschleicher
Copy link
Contributor

@bmagyar What do you think? Could this be merged?

Should I preserve the old behaviour for stop_time=0?

Adolfo-rt suggests in a comment, to preserve the old behaviour, if stop_time is zero. For me that could be an appropriate compromise

@@ -755,12 +762,14 @@ setHoldPosition(const ros::Time& time, RealtimeGoalHandlePtr gh)
const unsigned int n_joints = joints_.size();
for (unsigned int i = 0; i < n_joints; ++i)
{
hold_start_state_.position[0] = joints_[i].getPosition();
hold_start_state_.velocity[0] = joints_[i].getVelocity();
// If there is a time delay in the system it is better to use calculate the hold trajectory starting from the
Copy link
Member

Choose a reason for hiding this comment

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

"better to use calculate the hold trajectory"

@bmagyar
Copy link
Member

bmagyar commented Jan 11, 2018

@jschleicher Yes, please preserve that behaviour.

Also, I know this request is pretty blunt, but can you think of more ways to add tests for your fix? One could be to ensure that old behaviour is preserved.

@jschleicher
Copy link
Contributor

Yes, I'll add that.

Thanks for the review.

Adding a topic to rrbot to set an upper bound allows to test
the actual position after setHoldPosition() has been called.
old behaviour for stop_trajectory_duration==0
@mathias-luedtke
Copy link
Contributor

mathias-luedtke commented Mar 5, 2018

The tests are blocked..
Please rebase on top of the current kinetic-devel branch and resolve the merge conflicts.

Thanks ;)

@jschleicher
Copy link
Contributor

I've added the requested test case, please review again.

@@ -138,6 +138,13 @@ starting(const ros::Time& time)
time_data.uptime = ros::Time(0.0);
time_data_.initRT(time_data);

// Initialize the desired_state with the current state on startup
Copy link
Contributor

Choose a reason for hiding this comment

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

This does not change the behavior, it gets overwritten in update anyway.

Copy link
Contributor

Choose a reason for hiding this comment

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

exactly: It gets overwritten on the next update(). But starting calls setHoldPosition three lines later and reads the desired_state (if stop_trajectory_duration_ != 0), so we have to initialize it beforehand.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I have just written this for clarification/justfication during the review :)

Copy link
Contributor

@mathias-luedtke mathias-luedtke left a comment

Choose a reason for hiding this comment

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

After reading through the dicussions in #144 again, the consensus was that the stop_trajectory_duration option is not ideal..

I think adding a parameter/flag might be better to not break existing behavior.

However, the code and the tests look good.

hold_start_state_.position[0] = joints_[i].getPosition();
hold_start_state_.velocity[0] = 0.0;
hold_start_state_.acceleration[0] = 0.0;
(*hold_trajectory_ptr_)[i].front().init(time.toSec(), hold_start_state_,
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: start_time could be used as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

yep.

@mathias-luedtke
Copy link
Contributor

mathias-luedtke commented Mar 5, 2018

The desired state is not aligned to the hardware resolution (the current state should be), so commanding the desired state might result in small alternating motions.

Perhaps we should command the current state (joint-wise) if commanding the desired state would change the direction of motion.
Update: This would need a stateful hold with recomputing/adapting the hold trajectory in every cycle step.

@mathias-luedtke
Copy link
Contributor

mathias-luedtke commented Mar 6, 2018

The time parameter of setHoldPosition holds time_data_.uptime (time of last update). However, the desired_state will be reached at time_data_.uptime+ time_data_.period.

So with regard to the non-RT timing, it might make sense to set a 2 segment trajectory:

  1. current_state @ time_data_.uptime
  2. desired_state @ time_data_.uptime + time_data_.period
  3. stop_state @ time_data_.uptime + time_data_.period + stop_trajectory_duration

Not sure if this really makes a difference..
However, this ensures that there is a valid sement for the next update cycle.

@mathias-luedtke
Copy link
Contributor

As discussed with @jschleicher directly, this PR fixes a bug and the new behavior should be the default. If legacy users rely on the old behavior, it can be enabled by setting stop_trajectory_duration to zero (which is the default anyway). @bmagyar, do you agree?

@jschleicher
Copy link
Contributor

The time parameter of setHoldPosition holds time_data_.uptime (time of last update). However, the desired_state will be reached

From the plots and the update function I'd say desired_state is written at time_data.uptime:

hw_iface_adapter_.updateCommand(time_data.uptime, time_data.period,

So the hold trajectory is (using this PR) calculated starting from desired_state@time_data_.uptime. The first published value on the stop trajectory is calculated on the next update-call @time_data_.uptime + time_data_.period automatically.

@@ -35,5 +35,5 @@
<test test-name="joint_trajectory_controller_test"
pkg="joint_trajectory_controller"
type="joint_trajectory_controller_test"
time-limit="85.0"/>
time-limit="95.0"/>
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 necessary?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, the additional test takes some more seconds. I think, some "sleep" in the old tests aren't really needed, I'll try to optimize a bit to stay at 85 seconds here.

type="joint_trajectory_controller_test"
time-limit="85.0"/>
type="joint_trajectory_controller_vel_test"
time-limit="120.0"/>
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 necessary?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is "rebase" on develop, picked the change from there to avoid merge conflicts.

@bmagyar
Copy link
Member

bmagyar commented Mar 6, 2018

Guys, I think you are very close (or there already) to the source of #144 . I looked at it now, I think the approach is good and I've changed my stance on the question of the flag: we should put this by default.

BTW I experience this jerk every time I switch from the gravity compensation controller on Tiago to the joint_trajectory_contorller (the arm doesn't have time to fall, only a wee change on encoders).

As this is more of a major change, allow me to come back to this again later this week.

* use start_time variable in setHoldPosition
* doxygen documentation in header file
* drop sleep() in testcases in favor of waiting for PREEMPTED/SUCCEEDED state
  to speedup the tests a bit and stay below the former timeout of 85s
@bmagyar
Copy link
Member

bmagyar commented Mar 13, 2018

@ipa-mdl you may merge when ready.

@mathias-luedtke
Copy link
Contributor

@jschleicher, regarding the timing:
The update function will compute the spline interpolation from the trajectory's starting state (at last uptime) to the final state (at last uptime + stop trajectory duration). So if the stop trajectoy duration is smaller than the cycle time, the spline interpolation will be skipped.
This does not matter for the postion interface, but it might be used for for the velocity and effort interface.

I guess for most users this should not be an issue because the stop trajectory duration should be big enough anyway for practical reasons.

Copy link
Contributor

@mathias-luedtke mathias-luedtke left a comment

Choose a reason for hiding this comment

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

Please let me know if you want change anything w.r.t. the timing.
However, I am ready to merge this as-is.

Are you okay with squash-merging?
If not, please rebase accordingly, especially the last commit.

@jschleicher
Copy link
Contributor

Yes, please squash-merge.
Thank you.

@mathias-luedtke mathias-luedtke merged commit b10c2b7 into ros-controls:kinetic-devel Mar 16, 2018
@mathias-luedtke
Copy link
Contributor

Finally 🎉
Thanks for your patience and the many iterations!

@bmagyar
Copy link
Member

bmagyar commented Mar 16, 2018

Thanks guys for the effort!

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

Successfully merging this pull request may close these issues.

None yet

4 participants