-
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
Function to convert trajectory_msgs::JointTrajectory into aikido::trajectory::Spline #147
Conversation
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 |
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 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
tonumControlledJoints
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!
src/control/ros/Conversions.cpp
Outdated
throw std::invalid_argument{"StateSpace must be non-null."}; | ||
|
||
const auto numControlledDofs = _space->getNumSubspaces(); | ||
if (_jointTrajectory.joint_names.size() != numControlledDofs) |
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.
Rename numControlledDofs
to numControlledJoints
.
src/control/ros/Conversions.cpp
Outdated
if (_jointTrajectory.points.size() < 2) | ||
{ | ||
throw std::invalid_argument{ | ||
"Trajectory must contain at least two or more waypoints."}; |
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.
Nit: "Trajectory must contain two or more waypoints."
src/control/ros/Conversions.cpp
Outdated
<< _jointTrajectory.joint_names[i] << "."; | ||
throw std::invalid_argument{message.str()}; | ||
} | ||
} |
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 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.
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.
@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?)
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.
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
ordof_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?
src/control/ros/Conversions.cpp
Outdated
else if (isVelocityRequired) | ||
numCoefficients = 4; // cubic | ||
else | ||
numCoefficients = 2; // linear; |
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.
Nit: Remove extra ;
.
src/control/ros/CMakeLists.txt
Outdated
# Dependencies | ||
# | ||
find_package(trajectory_msgs REQUIRED) | ||
aikido_check_package(trajectory_msgs "aikido::control::ros" "trajectory_msgs") |
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.
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 @@ | |||
|
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.
Add a check here to only build these tests if the ros::control
component was built.
FYI, the
and similarly for |
@Shushman Yeah that's because it tries to match joint names with dof names. Fixing it now. :D |
I don't know what that means, but I'm glad @gilwoolee figured it out! 😈 |
tests/control/ros/CMakeLists.txt
Outdated
@@ -0,0 +1,6 @@ | |||
if(${FOUND_${PROJECT_NAME}_control_ros}) |
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.
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.
src/control/ros/Conversions.cpp
Outdated
if (n != 1) | ||
{ | ||
std::stringstream message; | ||
message << "[Conversion] Expected 1 dof. Joint " |
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.
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.
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.
👍 For removing the heading here.
Changes Unknown when pulling 6714011 on convertJointTrajectory into ** on master**. |
Changes Unknown when pulling 6714011 on convertJointTrajectory into ** on master**. |
Changes Unknown when pulling 785871b on convertJointTrajectory into ** 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. |
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.
Nits:
- "aikido's Spline Trajectory" -> "aikido's Spline trajectory"
- "single-dof" -> "single-DOF"
- Add a note about only handling
Rn
andSO2
joints.
src/control/ros/Conversions.cpp
Outdated
std::stringstream message; | ||
message << "Expected 1 dof. Joint " << i << " has " << n << " dofs."; | ||
throw std::invalid_argument{message.str()}; | ||
} |
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.
Check that getJointSpace(i)
is Rn
or SO2
.
src/control/ros/Conversions.cpp
Outdated
if (n != 1) | ||
{ | ||
std::stringstream message; | ||
message << "Expected 1 dof. Joint " << i << " has " << n << " dofs."; |
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.
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.
src/control/ros/Conversions.cpp
Outdated
rosJointToMSSSJoint.emplace(std::make_pair(i, j)); | ||
found_match = true; | ||
break; | ||
} |
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.
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 Joint
s from multiple Skeleton
s, throw
an exception. Then, this code simply becomes:
auto metaSkeleton = space->getMetaSkeleton();
auto joint = getJointByName(*metaSkeleton, jointName);
auto index = metaSkeleton->getIndex(joint);
src/control/ros/Conversions.cpp
Outdated
extractJointTrajectoryPoint(jointTrajectory, iwaypoint, numControlledJoints, | ||
&nextPosition, isPositionRequired, | ||
&nextVelocity, isVelocityRequired, | ||
&nextAcceleration, isAccelerationRequired); |
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'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.
src/control/ros/Conversions.cpp
Outdated
} | ||
} | ||
|
||
} // namespace |
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 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) |
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.
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); | ||
} |
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.
- Add a test for a
MetaSkeleton
that has multiple joints with the same name. This is possible by creating aGroup
that contains theJoint
with the same name in two differentSkeleton
s. - Add a test for a
MetaSkeleton
that is missing one of the joints named in theJointTrajectoryMessage
. - Add a test for a
JointTrajectory
that contains duplicate joints.
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.
Last round of (hopefully) minor comments! 😓
src/control/ros/Conversions.cpp
Outdated
{ | ||
if (metaSkeleton->getJoint(i)->getName() == jointName) | ||
return metaSkeleton->getJoint(i); | ||
} |
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.
Check if the MetaSkeleton
has more than one joint with this name.
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.
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.
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.
@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. 😉
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.
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.
Thanks @jslee02!
src/control/ros/Conversions.cpp
Outdated
|
||
//============================================================================= | ||
dart::dynamics::Joint* findJointByName( | ||
dart::dynamics::MetaSkeleton* metaSkeleton, const std::string& jointName) |
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.
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 &
.
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.
Hm, I know polymorphisms work both for pointers and references, but I thought our convention is to take a const pointer.
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.
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? 😬
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 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. 😅
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.
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.
src/control/ros/Conversions.cpp
Outdated
throw std::invalid_argument{message.str()}; | ||
} | ||
|
||
// Check that all joints are single DOF. |
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.
Nit: "...RnJoint or SO2Joint state spaces."
src/control/ros/Conversions.cpp
Outdated
|
||
for (size_t i = 0; i < jointTrajectory.joint_names.size(); ++i) | ||
{ | ||
std::string jointName = jointTrajectory.joint_names[i]; |
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.
Nit: const auto&
.
src/control/ros/Conversions.cpp
Outdated
auto index = metaSkeleton->getIndexOf(joint); | ||
assert(index != dart::dynamics::INVALID_INDEX); | ||
|
||
rosJointToMetaSkeletonJoint.emplace(std::make_pair(i, index)); |
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.
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
?
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 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.
src/control/ros/Conversions.cpp
Outdated
{ | ||
if (metaSkeleton.getJoint(i)->getName() == jointName) | ||
{ | ||
joints.emplace_back(metaSkeleton.getJoint(i)); |
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.
Nit: I'd prefer to store metaSkeleton->getJoint(i)
in a variable so we don't have to call it twice.
src/control/ros/Conversions.cpp
Outdated
<< joint_names[i] << "]."; | ||
throw std::invalid_argument{message.str()}; | ||
} | ||
} |
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.
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)) { /* ... */ }
src/control/ros/Conversions.cpp
Outdated
for (size_t i = 0; i < numControlledJoints; ++i) | ||
{ | ||
joint_names.emplace_back(jointTrajectory.joint_names[i]); | ||
} |
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.
Nit: Why is this loop necessary? I believe this is equivalent to:
auto joint_names = jointTrajectory.joint_names;
src/control/ros/Conversions.cpp
Outdated
auto metaSkeleton = space->getMetaSkeleton(); | ||
auto nJoints = jointTrajectory.joint_names.size(); | ||
|
||
for (size_t trajIndex = 0; trajIndex < nJoints; ++trajIndex) |
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.
Nit: The name trajIndex
is ambiguous, since it could refer to a waypoint index or a joint index.
src/control/ros/Conversions.cpp
Outdated
const auto& jointName = jointTrajectory.joint_names[trajIndex]; | ||
auto joints = findJointByName(*metaSkeleton, jointName); | ||
|
||
if (joints.size() == 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.
Nit: joints.empty()
.
src/control/ros/Conversions.cpp
Outdated
{ | ||
std::stringstream message; | ||
message << "Joint " << jointName << " (index: " << trajIndex << ")" | ||
<< " does not exist in metaSkeleton."; |
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.
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."
src/control/ros/Conversions.cpp
Outdated
{ | ||
std::stringstream message; | ||
message << "Multiple (" << joints.size() | ||
<< ") joints have the same name [" << jointName << "]."; |
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.
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."
Closing. Thanks a lot @mkoval @jslee02 @ClintLiddick ! |
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 usingJointTrajectory
, notMutliDOFJointTrajectory
?@mkoval @ClintLiddick Please confirm. If that's the case, I'll change the variable names, docstring, and name-checking part to clarify this.