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

Function to convert trajectory_msgs::JointTrajectory into aikido::trajectory::Spline #147

Merged
merged 16 commits into from
Apr 12, 2017

Conversation

gilwoolee
Copy link
Contributor

This PR branches from #111.
I have one question. It seems like the use of DOF and Joint is mixed here. convertJointTrajectory asks for _space->getNumSubSpaces() which would return the number of Joints, but then considers these as DOFs. This would work fine (except for name-matching) if all joints are single-dof joints. Is that the case since we're using JointTrajectory, not MutliDOFJointTrajectory?

@mkoval @ClintLiddick Please confirm. If that's the case, I'll change the variable names, docstring, and name-checking part to clarify this.

@gilwoolee gilwoolee requested a review from mkoval April 2, 2017 23:14
@mkoval mkoval mentioned this pull request Apr 2, 2017
@ClintLiddick
Copy link
Member

I took a look and I'm not exactly sure. I think this is a latent bug or, like you said, implicitly assuming a single-DOF joint (for JointTrajectory not MultiDOFJointTrajectory). I think if we gave this a true multi-DOF joint it would throw an incorrect error. Correct thing is probably to change the strings + docs + error message, and add a comment that this only works for single-DOF joints (i.e. no SO3/SE3 I think). I'm not quite sure what the right logic would be for handling multi-DOF joints, or if it could live in the same conversion functions. @mkoval thoughts?

@gilwoolee gilwoolee self-assigned this Apr 4, 2017
@jslee02 jslee02 added this to the Aikido 0.1.0 milestone Apr 4, 2017
Copy link
Member

@mkoval mkoval left a comment

Choose a reason for hiding this comment

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

This generally looks good. It's not surprising that I think so, since I originally wrote it! 😁 @gilwoolee, @jslee02, or @psigen could you review this as well to catch any errors I made?

@gilwoolee and @ClintLiddick are correct that this code only handles single-DOF joints. There is not a standard way of representing a multi-DOF joint in a trajectory_msgs/JointTrajectory message. We jump through a lot of gymnastics in Aikido (notably: representing the trajectory in the tangent space) to handle this.

I suggest:

  • Renaming numControlledDofs to numControlledJoints to make this clear.
  • Adding a check that raises an exception if there is a multi-DOF joint in the _stateSpace.
  • Revisiting this decision later if necessary.

Thanks @gilwoolee for splitting this into a separate pull request!

throw std::invalid_argument{"StateSpace must be non-null."};

const auto numControlledDofs = _space->getNumSubspaces();
if (_jointTrajectory.joint_names.size() != numControlledDofs)
Copy link
Member

Choose a reason for hiding this comment

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

Rename numControlledDofs to numControlledJoints.

if (_jointTrajectory.points.size() < 2)
{
throw std::invalid_argument{
"Trajectory must contain at least two or more waypoints."};
Copy link
Member

Choose a reason for hiding this comment

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

Nit: "Trajectory must contain two or more waypoints."

<< _jointTrajectory.joint_names[i] << ".";
throw std::invalid_argument{message.str()};
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This check will fail if _jointTrajectory and _space have the same joints, but in a different order. We should build a map from joint name to subspace and use it here.

Also, add a check that throws an exception if any of the joints are not single DOF.

Copy link
Contributor Author

@gilwoolee gilwoolee Apr 5, 2017

Choose a reason for hiding this comment

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

@mkoval I am a bit confused about your first comment. If we can't assume that they are given in the same order, how do we build the initial mapping? Should we compare each _jointTrajectory.joint_names[i]with all joint names in _space->getJointSpace(j)->getJoint()->getName()?

One more question: is it joint_name or dof_name that we get from _jointTrajectory.joint_names[] when we get it from actual HERB? (Assuming single dofs. Are they the same?)

Copy link
Member

Choose a reason for hiding this comment

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

If we can't assume that they are given in the same order, how do we build the initial mapping? Should we compare each _jointTrajectory.joint_names[i] with all joint names in _space->getJointSpace(j)->getJoint()->getName()?

That was my idea. None of these solutions are perfect, but I trust the names to be correct more than I trust the indices to be.

One more question: is it joint_name or dof_name that we get from _jointTrajectory.joint_names[] when we get it from actual HERB?

There is no correct answer here, since the JointTrajectory message has no distinction between joints and DOFs. I would strongly prefer Joint, since that: (1) it seems that DegreeOfFreedom names are typically (but not always) constructed from their Joint's name, (2) URDF only provides a mechanism for specifying joint names, and (3) it semantically matches the joint_name field in the message.

(Assuming single dofs. Are they the same?)

They are not guaranteed to be the same in DART, but I believe they are by default. @jslee02 Is that correct?

else if (isVelocityRequired)
numCoefficients = 4; // cubic
else
numCoefficients = 2; // linear;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Remove extra ;.

# Dependencies
#
find_package(trajectory_msgs REQUIRED)
aikido_check_package(trajectory_msgs "aikido::control::ros" "trajectory_msgs")
Copy link
Member

Choose a reason for hiding this comment

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

The control/ros namespace is intended to be optional, so traectory_msgs should not be marked as REQUIRED. We should change this to:

find_package(trajectory_msgs QUIET)
aikido_check_package(geometry_msgs "control::ros" "trajectory_msgs")

@@ -0,0 +1,6 @@

Copy link
Member

Choose a reason for hiding this comment

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

Add a check here to only build these tests if the ros::control component was built.

@mkoval mkoval changed the title Convert ROS/trajectory_msgs/JointTrajectory into aikido/trajectory/Spline Function to convert trajectory_msgs::JointTrajectory into aikido::trajectory::Spline Apr 4, 2017
@Shushman
Copy link
Contributor

Shushman commented Apr 4, 2017

FYI, the test_Conversions is failing here with the following error:

[ RUN      ] ConvertJointTrajectoryTests.LinearTrajectory
unknown file: Failure
C++ exception with description "Unknown joint name: expected Joint1, got DegreeOfFreedom1." thrown in the test body.
[  FAILED  ] ConvertJointTrajectoryTests.LinearTrajectory (0 ms)

and similarly for CubicTrajectory and QuinticTrajectory

@gilwoolee
Copy link
Contributor Author

@Shushman Yeah that's because it tries to match joint names with dof names. Fixing it now. :D

@mkoval
Copy link
Member

mkoval commented Apr 5, 2017

Yeah that's because it tries to match joint names with dof names.

I don't know what that means, but I'm glad @gilwoolee figured it out! 😈

@coveralls
Copy link

Coverage Status

Coverage remained the same at 85.089% when pulling 22ba7b4 on convertJointTrajectory into 6779dd2 on master.

@@ -0,0 +1,6 @@
if(${FOUND_${PROJECT_NAME}_control_ros})
Copy link
Member

@jslee02 jslee02 Apr 5, 2017

Choose a reason for hiding this comment

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

aikido_control_ros is a local target so FOUND_aikido_control_ros wouldn't be defined. Please use if(TARGET "${PROJECT_NAME}_control_ros") instead.

if(TARGET target-name)
True if the given name is an existing logical target name such as those created by the add_executable(), add_library(), or add_custom_target() commands.

if (n != 1)
{
std::stringstream message;
message << "[Conversion] Expected 1 dof. Joint "
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Only this warning message has the heading [Conversion]. It would good to be consistent by either removing the heading from here or adding the heading to all other messages.

Copy link
Member

Choose a reason for hiding this comment

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

👍 For removing the heading here.

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling 6714011 on convertJointTrajectory into ** on master**.

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling 6714011 on convertJointTrajectory into ** on master**.

@gilwoolee
Copy link
Contributor Author

@jslee02 Thanks for a quick review! @mkoval Would you mind giving it another pass? :D Thanks a lot!!

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling 785871b on convertJointTrajectory into ** on master**.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.007%) to 85.215% when pulling c98e8ea on convertJointTrajectory into a9b50ae on master.

/// This method only handles single-dof joints.
/// \param[in] _space MetaSkeletonStateSpace for Spline trajectory.
/// \param[in] _jointTrajectory ROS JointTrajectory to be converted.
/// \return Spline trajectory.
Copy link
Member

Choose a reason for hiding this comment

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

Nits:

  • "aikido's Spline Trajectory" -> "aikido's Spline trajectory"
  • "single-dof" -> "single-DOF"
  • Add a note about only handling Rn and SO2 joints.

std::stringstream message;
message << "Expected 1 dof. Joint " << i << " has " << n << " dofs.";
throw std::invalid_argument{message.str()};
}
Copy link
Member

Choose a reason for hiding this comment

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

Check that getJointSpace(i)is Rn or SO2.

if (n != 1)
{
std::stringstream message;
message << "Expected 1 dof. Joint " << i << " has " << n << " dofs.";
Copy link
Member

Choose a reason for hiding this comment

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

Nit: It would be helpful to include joint->getName() and joint->getType() to message, e.g.:

Only single-DOF joints are supported. Joint "my_joint1" (index: 1) is a SphericalJoint with 3 DOFs.

rosJointToMSSSJoint.emplace(std::make_pair(i, j));
found_match = true;
break;
}
Copy link
Member

Choose a reason for hiding this comment

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

Encapsulate this in a helper function. Putting this search inline means that we can't easily improve it's efficiency later (e.g. using unordered_map) and makes it harder to understand this function.

I suggest writing a helper function to get a Joint by name from a MetaSkeleton. If the name is ambiguous, which can occur in a MetaSkeleton that contain Joints from multiple Skeletons, throw an exception. Then, this code simply becomes:

    auto metaSkeleton = space->getMetaSkeleton();
    auto joint = getJointByName(*metaSkeleton, jointName);
    auto index = metaSkeleton->getIndex(joint);

extractJointTrajectoryPoint(jointTrajectory, iwaypoint, numControlledJoints,
&nextPosition, isPositionRequired,
&nextVelocity, isVelocityRequired,
&nextAcceleration, isAccelerationRequired);
Copy link
Member

Choose a reason for hiding this comment

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

I'd like to perform the reorder operation here. These two operations are closely related because they convert from a ROS data structure to the Aikido data structure that we expect. The rest of this loop purely performs operations on Aikido data structures, e.g. fitting splines.

}
}

} // namespace
Copy link
Member

Choose a reason for hiding this comment

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

I suspect that using a permutation matrix here will be a lot slower than operating directly on the list of std::pair<size_t, size_t>s. I also think it is is more complicated to implement this way, since it involves an intermediate data structure (the permutation matrix) that would not be otherwise necessary.

Since this is an entirely an implementation detail, I am fine keeping it as it is for now. However, we may want to consider refactoring this in the future if performance becomes an issue.

EXPECT_EIGEN_EQUAL(make_vector(6.), values, kTolerance);
}

TEST_F(ConvertJointTrajectoryTests, LinearTrajectoryWithDifferentOrdering)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: The fact that you are using a linear trajectory in this test is an implementation detail. I suggest renaming the test to just DifferentJointOrdering.


trajectory->evaluateDerivative(0.5, 2, values);
EXPECT_EIGEN_EQUAL(make_vector(0., 0.), values, kTolerance);
}
Copy link
Member

@mkoval mkoval Apr 8, 2017

Choose a reason for hiding this comment

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

  • Add a test for a MetaSkeleton that has multiple joints with the same name. This is possible by creating a Group that contains the Joint with the same name in two different Skeletons.
  • Add a test for a MetaSkeleton that is missing one of the joints named in the JointTrajectoryMessage.
  • Add a test for a JointTrajectory that contains duplicate joints.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.05%) to 85.259% when pulling d4bdaa3 on convertJointTrajectory into a9b50ae on master.

Copy link
Member

@mkoval mkoval left a comment

Choose a reason for hiding this comment

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

Last round of (hopefully) minor comments! 😓

{
if (metaSkeleton->getJoint(i)->getName() == jointName)
return metaSkeleton->getJoint(i);
}
Copy link
Member

Choose a reason for hiding this comment

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

Check if the MetaSkeleton has more than one joint with this name.

Copy link
Member

Choose a reason for hiding this comment

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

As a note, I'm thinking of implementing this to dart::dynamics::MetaSkeleton. That function would return a list of joint pointers whoes names are all jointName because MetaSkeleton possibly has multiple joints with the same name.

Copy link
Member

@mkoval mkoval Apr 11, 2017

Choose a reason for hiding this comment

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

@jslee02 That would be a great addition. 👍

In fact, it would be nice to have two functions: (1) one that returns a list of joints and (2) one that returns a single joint and prints a warning [or returns nullptr?] if multiple joints have the same name.

In any case, I don't want to wait on a new version of DART before merging this pull request. So we should also implement it here. 😉

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Thanks @jslee02!


//=============================================================================
dart::dynamics::Joint* findJointByName(
dart::dynamics::MetaSkeleton* metaSkeleton, const std::string& jointName)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: metaSkeleton is not an out parameter, so we should take it by reference. In this case, I think we can actually take it const &.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, I know polymorphisms work both for pointers and references, but I thought our convention is to take a const pointer.

Copy link
Member

Choose a reason for hiding this comment

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

The convention I've seen (and generally follow) is to prefer references and only use pointers for: (1) output parameters or (2) optional parameters that may be nullptr.

@psigen Want to break the tie? 😬

Copy link
Member

Choose a reason for hiding this comment

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

I haven't decided my preference here yet. If we want to use references for this case, then I see there are tons of changes in Aikido code base we need to make for the consistency. Just saying. 😅

Copy link
Member

Choose a reason for hiding this comment

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

Are you sure? Note that State variables are an exception to this rule because I did some sketchy business to reduce their memory footprint and am not sure whether or not calling *state to create a reference invokes undefined behavior.

throw std::invalid_argument{message.str()};
}

// Check that all joints are single DOF.
Copy link
Member

Choose a reason for hiding this comment

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

Nit: "...RnJoint or SO2Joint state spaces."


for (size_t i = 0; i < jointTrajectory.joint_names.size(); ++i)
{
std::string jointName = jointTrajectory.joint_names[i];
Copy link
Member

Choose a reason for hiding this comment

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

Nit: const auto&.

auto index = metaSkeleton->getIndexOf(joint);
assert(index != dart::dynamics::INVALID_INDEX);

rosJointToMetaSkeletonJoint.emplace(std::make_pair(i, index));
Copy link
Member

Choose a reason for hiding this comment

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

Verify that this is not a duplicate, i.e. that rosJointToMetaSkeletonJoint[i] does not already exist. You can do this by checking the bool flag in the std::pair returned by emplace.

Nit: Also, it would be nice to more descriptive variable names than i and index. 😉 Maybe metaSkeletonIndex and trajectoryIndex?

@coveralls
Copy link

Coverage Status

Coverage increased (+0.05%) to 85.259% when pulling 3a10630 on convertJointTrajectory into a59975f on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.01%) to 85.223% when pulling 8f1a94b on convertJointTrajectory into a59975f on master.

Copy link
Member

@mkoval mkoval left a comment

Choose a reason for hiding this comment

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

This looks great. Thanks @gilwoolee for putting up with the deluge of our comments. 😅

I left a few minor style comments. Feel free to address or ignore.

{
if (metaSkeleton.getJoint(i)->getName() == jointName)
{
joints.emplace_back(metaSkeleton.getJoint(i));
Copy link
Member

Choose a reason for hiding this comment

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

Nit: I'd prefer to store metaSkeleton->getJoint(i) in a variable so we don't have to call it twice.

<< joint_names[i] << "].";
throw std::invalid_argument{message.str()};
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Nit: it would be cleaner to replace this loop with:

auto duplicate_it = std:: adjacent_find(std::begin(joint_names), std::end(joint_names));
if (duplicate_it != std::end(joint_names)) { /* ... */ }

for (size_t i = 0; i < numControlledJoints; ++i)
{
joint_names.emplace_back(jointTrajectory.joint_names[i]);
}
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Why is this loop necessary? I believe this is equivalent to:

auto joint_names = jointTrajectory.joint_names;

auto metaSkeleton = space->getMetaSkeleton();
auto nJoints = jointTrajectory.joint_names.size();

for (size_t trajIndex = 0; trajIndex < nJoints; ++trajIndex)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: The name trajIndex is ambiguous, since it could refer to a waypoint index or a joint index.

const auto& jointName = jointTrajectory.joint_names[trajIndex];
auto joints = findJointByName(*metaSkeleton, jointName);

if (joints.size() == 0)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: joints.empty().

{
std::stringstream message;
message << "Joint " << jointName << " (index: " << trajIndex << ")"
<< " does not exist in metaSkeleton.";
Copy link
Member

Choose a reason for hiding this comment

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

Nit: This index is ambiguous because it could refer to an index in the JointTrajectory or an index in the MetaSkeleton. I suggest explicitly saying "trajectory index."

{
std::stringstream message;
message << "Multiple (" << joints.size()
<< ") joints have the same name [" << jointName << "].";
Copy link
Member

Choose a reason for hiding this comment

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

Nit: This message is ambiguous because it could refer to duplicate joints in the JointTrajectory or the MetaSkeleton. I suggest explicitly saying "in the JointTrajectory."

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.02%) to 85.189% when pulling 4fb24a5 on convertJointTrajectory into a59975f on master.

@gilwoolee gilwoolee merged commit 3c9c8cc into master Apr 12, 2017
@gilwoolee
Copy link
Contributor Author

Closing. Thanks a lot @mkoval @jslee02 @ClintLiddick !

@jslee02 jslee02 deleted the convertJointTrajectory branch April 15, 2017 00:33
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.

6 participants