-
Notifications
You must be signed in to change notification settings - Fork 30
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
Add Instantaneous and Queued TrajectoryExecutors #259
Conversation
This hasn't been tested yet!
This hasn't been tested yet either!
Codecov Report
@@ Coverage Diff @@
## master #259 +/- ##
==========================================
- Coverage 70.43% 68.98% -1.46%
==========================================
Files 182 186 +4
Lines 5399 5494 +95
Branches 847 861 +14
==========================================
- Hits 3803 3790 -13
- Misses 1076 1187 +111
+ Partials 520 517 -3
|
This almost works when I'm running some scripts in simulation. I'm running into issues while using The problem is that The actual queuing logic seems fine; if I comment out the skeleton locking/unlocking, the wrapped |
Ack, just figured out what was happening. In |
231b8a8
to
c4421fe
Compare
This commit moves the error-checking logic out of TrajectoryExecutor::execute into a new TrajectoryExecutor::validate method. This allows the locked error-checking logic to be performed when the lock is not held (outside of TrajectoryExecutor::step), enabling this to work for both the QueuedTrajectoryExecutor as well as the unqueued TrajectoryExecutors. In addition, this introduces a TrajectoryMetadata struct, which should replace prpy's trajectory Tags.
@mkoval When you get a chance, would you mind looking over my proposed fix for this locking issue in 06672e5? The short version is that the DOF validation that is performed by Instead, I'm now performing the DOF validation in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This generally looks okay to me. I am not sure how useful these features will be - but they are implemented pretty well - so I don't really have any qualms about merging this after a few minor comments are addressed.
Specific questions:
- In almost all cases, we should use MAGI to handle this queuing instead of
TrajectoryExecutor
. - The best reason to add queuing to the
TrajectoryExecutor
API is to support trajectory queuing and preemption in hardware drivers - which this PR doesn't support. - What is the purpose of the trajectory tag added in this PR?
/// Constructor. | ||
/// \param skeleton Skeleton to execute trajectories on. | ||
/// All trajectories must have dofs only in this skeleton. | ||
InstantaneousTrajectoryExecutor(::dart::dynamics::SkeletonPtr skeleton); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mark this as explicit
to disable implicit conversions from SkeletonPtr
.
Nit: Could we use a MetaSkeleton
instead of a Skeleton
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DART MetaSkeleton
s don't come with an associated mutex, so we wouldn't be able to lock that mutex while checking whether the DOFs specified by the trajectory are in the skeleton. It seems necessary to lock the mutex while doing that. I'd otherwise be happy to use MetaSkeleton
over Skeleton
.
@jslee02 why don't MetaSkeleton
s have mutexes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we have a good reason for not having a mutex in MetaSkeleton
. Probably the mutex in Skeleton
was added when MetaSkeleton
wasn't yet introduced, and we just didn't add one to MetaSkeleton
in introducing it?
@mxgrey Do you know why?
If we want to add a mutex to MetaSkeleton
, I think we can just move the mutex in Skeleton
to MetaSkeleton
.
/// should be all in skeleton passed to the constructor. | ||
/// \return future<void> for trajectory execution. If trajectory terminates | ||
/// before completion, future will be set to a runtime_error. | ||
/// \throws invalid_argument if traj is invalid. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does execute()
throw invalid_argument
itself? Or is the future<void>
set to an exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It currently throws the invalid_argument
immediately, rather than setting the future<void>
to an exception. I was thinking that we should throw the error as early as possible, but I could see it in either place. What do you think?
public: | ||
/// Constructor | ||
/// \param executor Underlying TrajectoryExecutor | ||
QueuedTrajectoryExecutor(std::unique_ptr<TrajectoryExecutor> executor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mark this as explicit
.
I am also not sure if we want to use a unique_ptr
here. I don't see why QueuedTrajectoryExecutor
requires exclusive ownership of the TrajectoryExecutor
that it delegates to.
void validate(trajectory::TrajectoryPtr traj) override; | ||
|
||
/// Execute trajectory and set future upon completion. If another trajectory | ||
/// is already running, queue the trajectory for later execution. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Define what happens if a trajectory earlier in the queue terminates with an error. Most likely, we should skip executing those trajectories and set their promise<>
s to an exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming that in the queue, one trajectory's endpoint is consistent with the beginning of the other, shouldn't we stop execution if one of them terminates with an error and set all the remaining promises to an exception? (This is what I do with SequenceExecutableSolution
in magi)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is something to just add to the simulation trajectory executors. I've created #272 to track this.
private: | ||
::dart::dynamics::SkeletonPtr mSkeleton; | ||
|
||
std::unique_ptr<std::promise<void>> mPromise; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This member variable seems to be unused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used by InstantaneousTrajectoryExecutor::execute
. I think it could be possible to avoid having this promise as a field in InstantaneousTrajectoryExecutor
and just have it as a local variable in execute
, if you think that would be better?
std::lock_guard<std::mutex> lock(mMutex); | ||
DART_UNUSED(lock); // Suppress unused variable warning | ||
|
||
mInProgress = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set mPromise
to an exception here to unblock anyone who is waiting on it.
trajectory::TrajectoryPtr traj) | ||
{ | ||
if (!traj) | ||
throw std::invalid_argument("Traj is null."); | ||
|
||
auto space = std::dynamic_pointer_cast<MetaSkeletonStateSpace>( | ||
if (traj->metadata.executorValidated) | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose of this check? Is there any harm in doing the validation again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how I'm attempting to avoid the deadlock issue I mentioned here: #259 (comment).
In libherb
, Herb::step
acquires the lock on the robot skeleton before calling QueuedTrajectoryExecutor::step
. If we re-validate, then we'll attempt to acquire the lock again which will cause a deadlock. I think a (dangerous?) alternative is to not lock the skeleton while doing this check.
catch (const std::exception& e) | ||
{ | ||
promise->set_exception(std::current_exception()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why manually copy the result of mFuture
to promise
, rather than directly returning mFuture
to the caller?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there are two types of futures: ones from a promise made by the QueuedTrajectoryExecutor
to the client, and ones from a promise made by the underlying TrajectoryExecutor
to the QueuedTrajectoryExecutor
.
I believe this distinction is necessary because the first type must be returned immediately after the client calls QueuedTrajectoryExecutor::execute
so that the client has something to wait
on. However, the second type isn't created until the underlying TrajectoryExecutor::execute
is called (when the trajectory is popped off the queue), so it can't be returned immediately by QueuedTrajectoryExecutor::execute
.
I think this means that I have to manually copy the result of mFuture
to promise
, but if there's another way I'd be happy to try that too!
In this implementation, mPromiseQueue
holds all the promises that have been made by the QueuedTrajectoryExecutor
to the client, while mFuture
is the current future that the QueuedTrajectoryExecutor
is waiting on (from the underlying TrajectoryExecutor
).
using aikido::statespace::dart::MetaSkeletonStateSpace; | ||
|
||
if (!traj) | ||
throw std::invalid_argument("Trajectory is null."); | ||
|
||
if (traj->metadata.executorValidated) | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above. Why is this necessary?
/// Execute traj and set future upon completion. | ||
/// Validate the traj in preparation for execution. | ||
/// \param traj Trajectory to be validated | ||
virtual void validate(trajectory::TrajectoryPtr traj) = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What exactly does it mean to validate a trajectory? Do we validate that it's collision free? Or that the robot's current state is in the same state as the beginning of the trajectory?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validating currently just means that the trajectory's DOFs match the DOFs from the skeleton that is being controlled by the TrajectoryExecutor
. It could do more though -- I was thinking of it as a function for checking things that can be checked before executing the trajectory (like whether the DOFs match up). That said, I only added this to try and work around the deadlock issue.
I think verifying that the current state is in the same state as the beginning of the trajectory wouldn't fall under this category; we want to check that immediately before we actually execute the trajectory. I've added #272 for that.
I think validating whether the trajectory is collision-free shouldn't be the job of the executor, since it has no idea what the world looks like while it's executing.
Thanks for the comments, @mkoval! ❤️
Agreed. I'm not sure whether these changes will be useful, but I figured since we already had a somewhat-working version of the code that I'd clean it up in case it's a good jumping-off point for something else in the future. I've added
This stemmed out of a conversation with @siddhss5 where he mentioned that we should have additional metadata associated with our trajectories (like we did in Adding this particular piece of metadata was just so that I could avoid adding an argument to |
I agree that this is potentially useful once we have more control over trajectory execution on real hardware in the future (cancelling, preempting, etc). 👍
If you make We can eventually support this on
Trajectory tags in general seem pretty useful for all the reasons that @siddhss5 gave. I'm not convinced that this is a great application of them, though. 😬 Unfortunately, I don't have a better solution to fixing the deadlock. |
Thanks! I've added
The options I've thought of are either (1) splitting up the validation like this [and either having this silly trajectory tag, or just moving this boolean from a trajectory tag to an |
My original intent with the |
Not really, we're actually still following your original intent. The tricky part is writing |
* Make KinematicSimulationTrajectoryExecutor and RosTrajectoryExecutor more similar. * Introduce TrajectoryRunningException. * Add InstantaneousTrajectoryExecutor. This hasn't been tested yet! * Add QueuedTrajectoryExecutor. This hasn't been tested yet either! * Fix typos. * Add InstantaneousTrajectoryExecutor tests. * Add QueuedTrajectoryExecutor tests. * Format code. * Fix locking issues. This commit moves the error-checking logic out of TrajectoryExecutor::execute into a new TrajectoryExecutor::validate method. This allows the locked error-checking logic to be performed when the lock is not held (outside of TrajectoryExecutor::step), enabling this to work for both the QueuedTrajectoryExecutor as well as the unqueued TrajectoryExecutors. In addition, this introduces a TrajectoryMetadata struct, which should replace prpy's trajectory Tags. * Make TrajectoryExecutor constructors explicit. * Support aborting queued trajectories. * Address more of Mike's review comments. * Add abort to TrajectoryExecutor interface. * Fix segfault when mPromise is null. * Fix formatting issues.
This is a better implementation than #251, following @mkoval's suggestions on that pull request. This PR also makes various changes to the
KinematicSimulation
andRos
TrajectoryExecutors
implementations to make them more consistent.Before creating a pull request
make format
Before merging a pull request
CHANGELOG.md