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

New vector field planner impelementation #268

Merged
merged 34 commits into from
Dec 10, 2017

Conversation

dqyi11
Copy link
Contributor

@dqyi11 dqyi11 commented Nov 26, 2017

This PR address the enhancement #267 .
The changes include

  • A new ConfigurationSpaceVectorField is created.
    • MoveEndEffectorOffsetVectorField is added
    • MoveEndEffectorPoseVectorField is added
  • A new VectorFieldPlanner class is created
    • Call boost::numeric::odeint::integrate_adaptive to integrate by adaptive step size
    • Add planToEndEffectorPose
  • Added test cases for planToEndEffectorPose

TODO
These will be added in another PR.

  • Collision checking of a trajectory (or segment) will be added (using sampling based method in prpy)

  • MoveEndEffectorAlongWorkspacePathVectorField will be added

  • planWorkspacePath will be added

  • Document new methods and classes

  • Format code with make format

Before merging a pull request

  • Set version target by selecting a milestone on the right side
  • Summarize this change in CHANGELOG.md
  • Add unit test(s) for this change

@dqyi11 dqyi11 added this to the Aikido 0.2.0 milestone Nov 26, 2017
@dqyi11 dqyi11 requested review from brianhou and jslee02 November 26, 2017 20:01
@codecov
Copy link

codecov bot commented Nov 26, 2017

Codecov Report

Merging #268 into master will increase coverage by 0.32%.
The diff coverage is 70.6%.

@@            Coverage Diff             @@
##           master     #268      +/-   ##
==========================================
+ Coverage   70.56%   70.88%   +0.32%     
==========================================
  Files         188      195       +7     
  Lines        5537     5605      +68     
  Branches      869      875       +6     
==========================================
+ Hits         3907     3973      +66     
- Misses       1097     1099       +2     
  Partials      533      533
Impacted Files Coverage Δ
include/aikido/statespace/StateSpace.hpp 100% <ø> (ø) ⬆️
.../aikido/statespace/dart/MetaSkeletonStateSpace.hpp 100% <ø> (ø) ⬆️
include/aikido/planner/vectorfield/VectorField.hpp 100% <100%> (ø)
...r/vectorfield/MoveEndEffectorOffsetVectorField.hpp 100% <100%> (ø) ⬆️
...do/planner/vectorfield/BodyNodePoseVectorField.hpp 100% <100%> (ø)
...anner/vectorfield/detail/VectorFieldIntegrator.hpp 100% <100%> (ø)
...ner/vectorfield/MoveEndEffectorPoseVectorField.hpp 100% <100%> (ø)
...ectorfield/detail/VectorFieldPlannerExceptions.cpp 18.18% <18.18%> (ø)
...ectorfield/detail/VectorFieldPlannerExceptions.hpp 20% <20%> (ø)
...r/vectorfield/MoveEndEffectorOffsetVectorField.cpp 48.71% <52.94%> (-7.29%) ⬇️
... and 17 more

@jslee02 jslee02 self-assigned this Nov 27, 2017

/// This class defines a vector field in a configuration space
///
/// Any vector filed should inherit this class to implememnt functions
Copy link
Member

Choose a reason for hiding this comment

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

typo: filed --> field

, mMetaSkeleton(_stateSpace->getMetaSkeleton())
, mBodyNode(_bodyNode)
{
}
Copy link
Member

Choose a reason for hiding this comment

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

Please move the function definitions to the .cpp file.

///
/// \param[out] _qd Joint velocities
/// \return Whether joint velocities are successfully computed
virtual bool getJointVelocities(Eigen::VectorXd& _qd) = 0;
Copy link
Member

Choose a reason for hiding this comment

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

I think this function can be a const function.

/// Vectorfield planning status callback function
///
/// \return Status of planning
virtual VectorFieldPlannerStatus checkPlanningStatus() = 0;
Copy link
Member

Choose a reason for hiding this comment

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

I think this function can be a const function.

@@ -2,58 +2,88 @@
#define AIKIDO_PLANNER_VECTORFIELD_VECTORFIELDPLANNER_HPP_

#include <functional>
#include <dart/common/Timer.hpp>
Copy link
Member

Choose a reason for hiding this comment

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

I guess you used this for debugging. We could remove this.

Copy link
Member

Choose a reason for hiding this comment

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

Never mind, dart::common::Timer is actually used in this class.

@@ -52,6 +53,7 @@ std::unique_ptr<aikido::trajectory::Spline> convertToSpline(
}

//==============================================================================

Copy link
Member

Choose a reason for hiding this comment

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

Nit: Adding empty line here is not our convention.

@@ -62,26 +64,32 @@ DesiredTwistFunction::DesiredTwistFunction(
}

//==============================================================================

Copy link
Member

Choose a reason for hiding this comment

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

Nit: Adding empty line here is not our convention.

double DesiredTwistFunction::eval(const Eigen::VectorXd& _qd)
{
return 0.5 * (mJacobian * _qd - mTwist).squaredNorm();
}

//==============================================================================

Copy link
Member

Choose a reason for hiding this comment

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

Nit: Adding empty line here is not our convention.

void DesiredTwistFunction::evalGradient(
const Eigen::VectorXd& _qd, Eigen::Map<Eigen::VectorXd> _grad)
{
_grad = mJacobian.transpose() * (mJacobian * _qd - mTwist);
}

//==============================================================================

Copy link
Member

Choose a reason for hiding this comment

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

Nit: Adding empty line here is not our convention.

Copy link
Member

@jslee02 jslee02 left a comment

Choose a reason for hiding this comment

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

I like this set of changes in terms of creating a class for planning algorithm and adding more useful vector field classes.

I've made some comments.

/// \return Status of planning
virtual VectorFieldPlannerStatus checkPlanningStatus() = 0;

/// Meta skeleton spate space
Copy link
Member

Choose a reason for hiding this comment

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

Typo: spate --> state

virtual VectorFieldPlannerStatus checkPlanningStatus() = 0;

/// Meta skeleton spate space
virtual aikido::statespace::dart::MetaSkeletonStateSpacePtr
Copy link
Member

@jslee02 jslee02 Nov 28, 2017

Choose a reason for hiding this comment

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

Why are these functions (and below getters) virtual? Any possibility that a subclass overrides this function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. I don't know why I wrote that way. Good catch! Thanks!

getMetaSkeletonStateSpace()
{
return mStateSpace;
}
Copy link
Member

@jslee02 jslee02 Nov 28, 2017

Choose a reason for hiding this comment

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

Please add const functions for

  • getMetaSkeletonStateSpace()
  • getMetaSkeleton()
  • getBodyNode()

that return pointers to const objects accordingly like aikido::statespace::dart::MetaSkeletonStateSpaceConstPtr getMetaSkeletonStateSpace() const where MetaSkeletonStateSpaceConstPtr is defined as using MetaSkeletonStateSpaceConstPtr = std::shared_ptr<const MetaSkeletonStateSpace>.

This is necessary for const-correctness. Without const functions of this getters, a const instance of ConfigurationSpaceVectorField might be able to modify the MetaSkeletonStateSpace through this getter for example, which is not an expected behavior from a const instance.

Edit: MetaSkeletonStateSpaceConstPtr --> ConstMetaSkeletonStateSpacePtr

Copy link
Contributor Author

@dqyi11 dqyi11 Nov 29, 2017

Choose a reason for hiding this comment

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

Should ConstPtr be defined in original header file? Fox example using MetaSkeletonStateSpaceConstPtr = std::shared_ptr<const MetaSkeletonStateSpace> in MetaSkeletonStateSpace.hpp?

double _optimizationTolerance = 1e-3,
double _padding = 1e-5);
const Eigen::Vector3d& _direction,
double _distance,
Copy link
Member

Choose a reason for hiding this comment

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

Nit: _minDistance to be consistent with _maxDistance?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is following the naming in prpy. This is usually the expected distance to move. The planner will terminate once this distance is reached. It will fail it _maxDistance is reached.

Copy link
Member

Choose a reason for hiding this comment

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

That is not what the planner currently does it PrPy. It integrates as far as possible until it either reaches _maxDistance or is unable to continue. Then, it returns success if the resulting trajectory has length greater than _distance. I.e. the planner will return a trajectory of length [_distance, _maxDistance] upon success.

I agree with @jslee02's naming suggestion - I think _minDistance is more descriptive.

///
/// \param[in] _stateSpace MetaSkeleton state space
/// \param[in] _bn Body node of end-effector
/// \param[in] _goalPose Desired en-effector pose
Copy link
Member

Choose a reason for hiding this comment

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

Typo: en-effector --> end-effector

///
/// \param[in] _q Position in configuration space
/// \param[in] _t Current time being planned
void check(const Eigen::VectorXd& _q, double _t);
Copy link
Member

Choose a reason for hiding this comment

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

I think step and check functions should be protected because they're just used internally in this class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

double _t,
Eigen::VectorXd& _dq)>;
VectorFieldPlanner(
const aikido::planner::vectorfield::ConfigurationSpaceVectorFieldPtr
Copy link
Member

Choose a reason for hiding this comment

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

Nit: We don't need to specify the whole namespaces. This can be simplified as

const ConfigurationSpaceVectorFieldPtr

Please remove aikido::[planner::[vectorfield]] in this header.

int mIndex;

bool mEnableCollisionCheck;
bool mEnableDofLimitCheck;
Copy link
Member

Choose a reason for hiding this comment

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

It seems mEnableCollisionCheck and mEnableDofLimitCheck are not actually used because they are always overwritten in followVectorField().

I think we should either (1) remove these member variables and always pass them as parameters of followVectorField() or (2) add getters and setters for them and remove those parameters from followVectorField(). I'm inclined to (2) because those parameters should represent state of the planner but not temporary variables.

If we choose (2) then we might want to set those parameters in the constrcutor.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These two member variables are actually used in check() and step(). These two functions are only internally called inside followVectorField(). These two parameters are about how you shall follow a vector field (like integral time), so I put them as input arguments of followVectorField(). They are set to member variables so that check() and step() could access. Any suggestion?

@@ -63,31 +93,81 @@ std::unique_ptr<aikido::trajectory::Spline> planPathByVectorField(
/// \param[in] _constraint Trajectory-wide constraint that must be satisfied
/// \param[in] _direction Direction of moving the end-effector
/// \param[in] _distance Distance of moving the end-effector
/// \param[in] _linearVelocity Linear velocity of moving the end-effector
/// \param[in] _linearTolerance How a planned trajectory is allowed to
/// \param[in] _maxDistance Max distance of moving the end-effector
Copy link
Member

Choose a reason for hiding this comment

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

I'm confused with what this function wants to do. Does this function plan a trajectory moving the EE for a certain distance (> distance & > maxDistance) along the given direction, which is my interpretation according to the logic of checkPlanningStatus().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Once the moved distance is larger than distance, it will terminate and return a trajectory.
But if the moved distance is larger than maxDistance, it violates and will fail.

Copy link
Member

Choose a reason for hiding this comment

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

It's different from my understanding of your implementation. According to this logic, the integration keeps going even when movedDistance is greater than mDistance (that is set by _maxDistance), and it only terminates when mDistance is greater than mMaxDistance. Am I missing something here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. You are right. I will update the doc strings accordingly :)

Copy link
Member

Choose a reason for hiding this comment

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

No, the logic that @jslee02 described is correct. See my other comment about what the behavior currently is in PrPy, which is what this function mimics.

I am happy to revisit that behavior 🙃 But I think it's quite useful in practice.

|| status == VectorFieldPlannerStatus::CACHE_AND_TERMINATE)
{
throw VectorFieldTerminated(
"Planning was terminated by the StatusCallback.");
Copy link
Member

Choose a reason for hiding this comment

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

Why do we treat the termination of planning (which I believe it's successfully terminated) as an exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right. This is the way of stopping integration before it is done. Usually we set 10 sec to integrate but can reach an expected distance before that. Any other suggested way?

double _optimizationTolerance = 1e-3);

/// Vectorfield callback function
///
/// \param[out] _qd Joint velocities
/// \return Whether joint velocities are successfully computed
virtual bool getJointVelocities(Eigen::VectorXd& _qd) override;
virtual bool getJointVelocities(Eigen::VectorXd& _qd) const override;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Please remove virtual. virtual is redundant for an overridden function.


/// Vectorfield planning status callback function
///
/// \return Status of planning
virtual VectorFieldPlannerStatus checkPlanningStatus() override;
virtual VectorFieldPlannerStatus checkPlanningStatus() const override;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Please remove virtual. virtual is redundant for an overridden function.

@@ -44,22 +44,22 @@ class MoveEndEffectorPoseVectorField : public ConfigurationSpaceVectorField
/// \param[in] _t Current time being planned
/// \param[out] _qd Joint velocities
/// \return Whether joint velocities are successfully computed
virtual bool getJointVelocities(Eigen::VectorXd& _qd) override;
virtual bool getJointVelocities(Eigen::VectorXd& _qd) const override;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Please remove virtual. virtual is redundant for an overridden function.


/// Vectorfield planning status callback function
///
/// \param[in] _q Position in configuration space
/// \param[in] _t Current time being planned
/// \return Status of planning
virtual VectorFieldPlannerStatus checkPlanningStatus() override;
virtual VectorFieldPlannerStatus checkPlanningStatus() const override;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Please remove virtual. virtual is redundant for an overridden function.

Copy link
Member

@jslee02 jslee02 left a comment

Choose a reason for hiding this comment

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

Looks good to me! 👍

@jslee02 jslee02 assigned dqyi11 and unassigned jslee02 Nov 30, 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.

Thanks for tackling this refactor @dqyi11!

Some high-level comments:

Integration Time. The time parameter used by the integrator is not meaningful because the planner does not consider dynamics. Therefore, integration time should be treated as an implementation detail of the integrator, i.e. not used as a termination condition or argument to the vector field. This may require calling the Stepper directly, rather than using the integrate_adaptive() helper method. Thankfully, that method does not actually do very much.

Non-Rn State Space Support. To be consistent with the rest of Aikido's planners, a vector field should be evaluated on a State - not an Eigen::VectorXd. This means that the integrator needs to use the operations defined on StateSpace (e.g. compose(), expMap(), logMap()) to interact with the states rather than vector operations. This is important to support configuration spaces with other topologies.

The correct way to implement this using Boost.Numeric.OdeInt is likely to configure the integrator to define custom StateType, Algebra, Operations types that internally call functions on the base Aikido StateSpace API. This should be straightforward because there is a relatively direct mapping between the Aikido API and the API desired by Boost.

Non-MetaSkeletonStateSpace Support. The base vector field planner should work on an arbitrary StateSpace using state space operations (see above). A MetaSkeletonStateSpace is only required when mapping from workspace to configuration space. See below for one idea about how to implement this.

Joint Limits and Collision Checking. These should be implemented using the Aikido constraint API. The easiest way of implementing this is likely to accept a Testable constraint. The user can then pass in the appropriate NonColliding and joint limit constraints for their robot.

Constraint Validation. The resolution at which constraints are checked should not be related to the integration step size. Those are independent quantities: constraints should be checked at a user defined resolution (e.g. chosen with knowledge of the robot's kinematics and the amount of padding on objects in the environment) and the latter is an internal parameter chosen by the integrator (e.g. larger when the vector field is flat and smaller when it's derivatives are large).

Conceptually, you can think about these as two separate operations: the planner first integrates a trajectory, then tests the entire trajectory for constraint satisfaction at a user-defined resolution. In practice, to be more efficient, we can interleave the two operations.

To do this, the planner must maintain two pieces of state: (1) the trajectory integrated so far and (2) the last point in the trajectory that was checked for constraint satisfaction. Then, after taking a step, the planner can check the new portion of the trajectory for validity. I can sketch this out in more detail if my explanation is not clear.


Here is a potential sketch of an API that makes the base VectorField class for for an arbitrary StateSpace:

  • A base VectorField class that operates on an arbitrary StateSpace.

    class VectorField {
    public:
      bool evaluateVelocity(const StateSpace::State* _state,
                            Eigen::VectorXd* _velocity) = 0;
    
      // Preferable: Status is a function of the entire trajectory so far. 
      VectorFieldPlannerStatus evaluateStatus(
        const aikido::trajectory::Trajectory& _trajectory) = 0;
    
      // Alternative: Status is only a function of the current state.
      VectorFieldPlannerStatus evaluateStatus(
        const StateSpace::State* _state) = 0;
    };
  • A BodyNodePoseVectorField defined for a MetaSkeletonStateSpace that converts a vector field for a BodyNode's pose (i.e. defined over SE(3)) to the robot's configuration space.

    class BodyNodePoseVectorField : public VectorField {
    public:
      BodyNodePoseVectorField(MetaSkeletonStateSpacePtr _stateSpace,
                              const BodyNode* _bodyNode);
    
      bool evaluatePoseVelocity(const SE3::State* _state,
                                Eigen::VectorXd* _velocity) = 0;
    
    protected:
      // Defined in terms of evaluatePoseVelocity().
      bool evaluateVelocity(const StateSpace::State* _state,
                            Eigen::VectorXd* _velocity);
    };
    
  • MoveEndEffectorOffsetVectorField and MoveEndEffectorPoseVectorField are defined using BodyNodePoseVectorField.

/// \param[in] _bn Body node of end-effector
ConfigurationSpaceVectorField(
aikido::statespace::dart::MetaSkeletonStateSpacePtr _stateSpace,
dart::dynamics::BodyNodePtr _bodyNode);
Copy link
Member

Choose a reason for hiding this comment

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

A generic vector field can be defined on any state space, not only MetaSkeletonStateSpaces. Also, it is not necessarily parameterized by a BodyNode. Those are implementation details of how some vector fields are implemented, e.g. those that move a BodyNode along a constraint. I think we should replace MetaSkeletonStateSpace with StateSpace and remove all of the reference to BodyNodes from this base class.

It would also be useful to introduce a subclass that handles the conversion from task space to configuration space. That class could take a BodyNodePtr and handle the necessary Jacobian calculations to convert a twist in SE(3) to the tangent space of the MetaSkeleton's configuration space.


/// This class defines a vector field in a configuration space
///
/// Any vector field should inherit this class to implememnt functions
Copy link
Member

Choose a reason for hiding this comment

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

Typo: "implememnt" -> "implement"

///
/// Any vector field should inherit this class to implememnt functions
/// for calculating joint velocities giving joint positions and time,
/// and checking planning status.
Copy link
Member

Choose a reason for hiding this comment

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

I don't know what "and time" means here. I think this should just say "joint positions."

To be pedantic, we shouldn't be referring to joints at all in this class. A vector field is defined over an abstract state space - it does not technically even have to be a MetaSkeletonStateSpace!

///
/// \param[out] _qd Joint velocities
/// \return Whether joint velocities are successfully computed
virtual bool getJointVelocities(Eigen::VectorXd& _qd) const = 0;
Copy link
Member

Choose a reason for hiding this comment

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

This should take a StateSpace::State * output parameter, not an Eigen::VectorXd.

/// Vectorfield planning status callback function
///
/// \return Status of planning
virtual VectorFieldPlannerStatus checkPlanningStatus() const = 0;
Copy link
Member

Choose a reason for hiding this comment

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

Where does this function get it's input from? This function should take a const StateSpace::State * as an argument at a minimum. Ideally, we would pass the entire trajectory that has already been integrated - but that would be significantly more difficult to implement.

double jointLimitTolerance = 3e-2,
double optimizationTolerance = 1.,
double timelimit = 5.0,
double integralTimeInterval = 10.0);
Copy link
Member

Choose a reason for hiding this comment

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

This helper function doesn't provide too much value over the functions that it wraps. I'd slightly prefer to remove function from Aikido and move it to libherb, where we can specify good defaults for HERB.

/// in planning.
/// \return A trajectory following the vector field.
std::unique_ptr<aikido::trajectory::Spline> plan(
double integrationTimeInterval,
Copy link
Member

Choose a reason for hiding this comment

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

The integration time parameter is meaningless (see my comment above), so it does not make sense to specifiy a limit here.

/// \return A trajectory following the vector field.
std::unique_ptr<aikido::trajectory::Spline> plan(
double integrationTimeInterval,
double timelimit,
Copy link
Member

Choose a reason for hiding this comment

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

We generally prefer to use std::chrono types to represent time durations. Consider using std::duration<T> or - if you don't want to template this function - std::duration<double>.

/// Define termination of vectorfield planner.
// When VectorFieldTerminated is throws, it means
// that planning is finished.
class VectorFieldTerminated : public std::exception
Copy link
Member

Choose a reason for hiding this comment

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

This class is an implementation detail - it is only necessary because we use an exception as a workaround to prematurely terminate the integration. This belongs in the .cpp file or a detail namespace.

/// \param[in] _cache_index Total cache index number
/// \param[in] _stateSpace MetaSkeleton state space
/// \param[in] knots A sequence of knots.
/// \param[in] cache_index Total cache index number.
Copy link
Member

Choose a reason for hiding this comment

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

I don't know what this means. If it's the length of the trajectory to keep, then I suggest removing the parameter entirely relying on the user to truncate knots. E.g. they could call knots.resize(cache_index) before calling this function

@dqyi11
Copy link
Contributor Author

dqyi11 commented Dec 4, 2017

@mkoval How about the below design of classes to support vector field planner in arbitrary StateSpace?

VectorField class provides evaluateVelocity() and evaluateStatus().
VectorFieldPlanner class provides evaluateTrajectory() and followVectorField().

Define an abstract vector field for any state space.

class VectorField
{
public:
  bool evaluateVelocity(const StateSpace::State* _state,
                        Eigen::VectorXd* _velocity) = 0;

  // Alternative: Status is only a function of the current state.
  VectorFieldPlannerStatus evaluateStatus(
    const StateSpace::State* _state) = 0;
};

Define a MetaSkeletonStateSpaceVectorField in robot's configuration space.
(Currently we do not have one, but it is possible that there will be applications that is directly defined in MetaSkeletonStateSpace, for example, something similar with planToConfiguration?)

class MetaSkeletonStateSpaceVectorField : public VectorField
{
public:
  MetaSkeletonStateSpaceVectorField(
      aikido::statespace::dart::MetaSkeletonStateSpacePtr stateSpace);
  bool evaluateVelocity(const StateSpace::State* _state,
                        Eigen::VectorXd* _velocity);

  // Alternative: Status is only a function of the current state.
  VectorFieldPlannerStatus evaluateStatus(
    const StateSpace::State* _state);
};

Define a BodyNodePoseVectorField for a type of vector field in robot's configuration space that is created from workspace.

class BodyNodePoseVectorField : public MetaSkeletonStateSpaceVectorField
{
BodyNodePoseVectorField(
      aikido::statespace::dart::MetaSkeletonStateSpacePtr stateSpace,
      dart::dynamics::BodyNodePtr bodyNode);
};

For example, define a MoveEndEffectorOffsetVectorField for a specific BodyNodePoseVectorField.

class MoveEndEffectorOffsetVectorField : public BodyNodePoseVectorField
{
};

Correspondingly, we could have classes of VectorFieldPlanner (will be in details) as below.
VectorFieldPlanner classes provides evalutateTrajectory() and followVectorField().

class VectorFieldPlanner
{
public:
  VectorFieldPlanner(
      const VectorFieldPtr vectorField,
      const aikido::constraint::TestablePtr constraint);
              
  bool evaluateTrajectory(
      const aikido::trajectory::Trajectory& _trajectory,
      const aikido::constraint::TestablePtr constraint,
      double evalTimeStep);

  std::unique_ptr<aikido::trajectory::Spline> followVectorField(
      const aikido::statespace::StateSpace::State* startState,
      double timelimit);
};

A MetaSkeletonStateSpaceVectorFieldPlanner for all the vector fields defined in robot's configuration space.
The evaluateTrajector() accepts only timed trajectory.
It checks the satisfactions of not only CollisionFree constraint and DOF limit constraint, but also joint velocity limit constraint.

class MetaSkeletonStateSpaceVectorFieldPlanner : public VectorFieldPlanner
{
public:
  VectorFieldPlanner(
      const MetaSkeletonStateSpaceVectorFieldPtr vectorField,
      const aikido::constraint::TestablePtr constraint);
};

It seems that Testable works with a State in state space. What is the best practice of using it to check velocity limit violation, the information of which is not expressed in a State?

@dqyi11
Copy link
Contributor Author

dqyi11 commented Dec 5, 2017

Changes include:

  • Integration time is no longer a parameter but a constant. Customizing stepper() will be addressed in Remove integration time as a termination condition in vector field planner #275.
  • Non-Rn State Space Support will be addressed in Support arbitrary state spaces in the VectorField planner #274
  • Non-MetaSkeletonStateSpace Support is done. New classes are added for vector fields in arbitrary state space.
  • Joint Limits and Collision Checking are now called by using Testable constraints.
    Eigen::VectorXd is replaced with aikido::statespace::StateSpace::State.
  • Constraint Validation is done using a different resolution parameter with integration step size.
    Our original plan was using a trajectory-wide constraint validation after vector field planner generates a path, so the constraint validation is following a rough observation interval (defined by the stepper()). But now evaludateTrajectory() is added to support trajectory-wide constraint validation. It is used to check in new segment in the planning process (needs a user-defined resolution parameter).

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.

Thanks for making those changes - I think we're converging towards a solid implementation. 😅 I think you are taking the correct approach by filing issues for difficult-to-implement features that we don't need immediately.

I am trying to focus only on issues that affect the public API and inheritance tree. These are the remaining major issues that I see:

  • Missing functions on BodyNodePoseVectorField. The intent of this class was to encapsulate the boilerplate required to convert a vector field over a BodyNode's pose into configuration space. To do that, the class needs to implement evaluateVelocity() and evaluateStatus() in terms of equivalent functions that operate on the BodyNode's pose.

    This change should significantly simplify the implementations of MoveEndEffectorOffsetVectorField and MoveEndEffectorPoseVectorField.

  • MetaSkeletonStateSpaceVectorField is unnecessary because it doesn't seem to provide any functionality unless combined with BodyNodePoseVectorField. If that is the case, we should merge that functionality directly into BodyNodePoseVectorField.

  • MetaSkeletonStateSpaceVectorFieldIntegrator is unnecessary** because all of the pure-virtual functions on VectorFieldIntegrator may be implemented for an arbitrary StateSpace. This is nice because it means that the vast majority of the planner can operate on an arbitrary state space.

  • Collision Free and Bounds Constraints. The vector field planner should operate on an opaque Testable constraint - it does not need access to individual collision and bounds constraints. If the goal is only to improve error messages, then that is something we can tackle using an approach similar to Testable Constraint Output #266.

  • Incremental Constraint Validation. The current constraint validation logic validates the trajectory only after the integration has terminated. This is suboptimal because we won't "fail fast" if we quickly encounter a constraint violation. This also completely breaks the "move as far as possible" paradigm that we often use on HERB, e.g. to move straight as far as possible to contact the environment.

  • Incorrect Constraint Validation. The constraint validation currently discretizes the trajectory using an integration time resolution. This is incorrect because integration time is meaningless - we should be collision checking at a fixed DOF resolution. The correct implementation requires Implement collision checking for Spline trajectories #212, so I proposed a few short-term workarounds.

  • Documentation. It would be helpful to do a pass and update docstrings, particularly on classes that the user will have to interact with (e.g. by overriding functions in subclasses). A number of docstrings are missing or outdated pending the most recent refactors.

///
/// Any vector field should inherit this class to implememnt functions
/// for calculating joint velocities giving joint positions and time,
/// and checking planning status.
Copy link
Member

Choose a reason for hiding this comment

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

This description is no longer correct. It should say something like this:

This class is a vector field over a MetaSkeletonStateSpace that is defined in terms of a Cartesian vector field over the pose of a BodyNode in that MetaSkeleton. The Jacobian pseudoinverse is used to map velocities in se(3) to velocities in configuration space. If multiple solutions exist, i.e. the kinematic chain leading to the BodyNode in the MetaSkeleton is redundant, then a heuristic is used to select velocities from the null space to avoid violating joint limits.

/// \param[in] initialStepSize Initial step size in integation.
MetaSkeletonStateSpaceVectorFieldIntegrator(
const MetaSkeletonStateSpaceVectorFieldPtr vectorField,
const aikido::constraint::TestablePtr constraint,
Copy link
Member

Choose a reason for hiding this comment

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

Nit: These arguments are passed by value, so it is meaningless to flag them as const.

// Documentation inherited.
bool evaluateTrajectory(
const aikido::trajectory::Trajectory* trajectory,
const aikido::constraint::TestablePtr collisionFreeConstraint,
Copy link
Member

Choose a reason for hiding this comment

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

Nits:

  • This function does not support trajectory being nullptr, so it would be cleaner to take that argument const & instead of a const *.
  • This function does not take ownership of collisionFreeConstraint , it would be cleaner to take that argument by const & instead of a shared_ptr.
  • This collisionFreeConstraint is currently passed by value, so it is meaningless to flag it is as const.
  • The name collisionFreeConstraint is misleading because it can be any constraint, not just a CollisionFree constraint.

/// than distance.
/// (2) When maxDistance is not set to InvalidMaxDistance,
/// planning will terminate and return a path, the path length
/// of which is [distance, maxDistance].
Copy link
Member

Choose a reason for hiding this comment

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

If I understand this correctly, it may be simpler to say that:

If not specified, maxDistance defaults to distance.

double angularTolerance = 0.15,
double linearVelocityGain = 1.0,
double initialStepSize = 5e-2,
double jointLimitPadding = 3e-2);
Copy link
Member

Choose a reason for hiding this comment

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

I suggest moving all of these defaults to libherb since they are highly robot specific.

/// Any vector field should inherit this class to implememnt functions
/// for calculating joint velocities giving joint positions and time,
/// and checking planning status.
class MetaSkeletonStateSpaceVectorField : public VectorField
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this class is necessary - it doesn't seem to provide any value over VectorField itself. I think we can safely roll all of this functionality into BodyNodePoseVectorField.

// Documentation inherited.
virtual bool evaluateVelocity(
const aikido::statespace::StateSpace::State* state,
Eigen::VectorXd& qd) const override = 0;
Copy link
Member

Choose a reason for hiding this comment

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

I was expecting this class to define evaluateVelocity() in terms of another function that returns the Cartesian velocity of the specified BodyNode. E.g.:

bool BodyNodePoseVectorField::evaluateVelocity(
      const aikido::statespace::StateSpace::State* state,
      Eigen::VectorXd& qd) const override = 0) override
{
  // Pseudocode. Actual implementation will require some massaging of data types.
  const auto cartesianState = mBodyNode->getPose(state);

  Eigen::Vector6d cartesianVelocity;
  if (!evaluateCartesianVelocity(cartesianState, cartesianVelocity))
    return false;

  return computeJointVelocityFromTwist(mBodyNode, cartesianVelocity, &qd);
}

// Implemented by the user:
virtual bool BodyNodePoseVectorField::evaluateCartesianVelocity(
  const Eigen::Isometry3d& _pose, Eigen::Vector6d& _cartesianVelocity) = 0;

In my opinion, the whole point of this class is to allow the user to define a vector field over the pose of a BodyNode without worrying about the boilerplate required to call `computeJointVelocityFromTwist().


// Documentation inherited.
virtual VectorFieldPlannerStatus evaluateStatus(
const aikido::statespace::StateSpace::State* state) const override = 0;
Copy link
Member

Choose a reason for hiding this comment

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

I was expecting this class to define evaluateStatus() in terms of another evaluateCartesianStatus() function that accepts the pose of the BodyNode. See my comment on evaluateVelocity() for details.

== mMetaSkeleton->getVelocityLowerLimits())
{
throw std::invalid_argument("Velocity space volume zero");
}
Copy link
Member

Choose a reason for hiding this comment

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

I don't think these checks are necessary. Bounds should be handled entirely by the Testable passed by the user.

/// This class defines two callback functions for a integrator.
/// step() provides joint velocities the vector field planner should follow,
/// check() is called after each integration step to check planner status.
class MetaSkeletonStateSpaceVectorFieldIntegrator : public VectorFieldIntegrator
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure why this class is necessary. I think we can safely delete this once we implement convertStateToPositions() and convertPositionsToState() in the base VectorField class.

@dqyi11
Copy link
Contributor Author

dqyi11 commented Dec 7, 2017

  • Missing functions on BodyNodePoseVectorField are added. evaluateVelocity() and evaluateStatus() are added that operate on the BodyNode's pose.
  • MetaSkeletonStateSpaceVectorField and MetaSkeletonStateSpaceVectorFieldIntegrator are removed.
  • Incremental Constraint Validation is supported. Now a trajectory is created from a new knot to the last knot in list, and is sent to evaluateTrajectory for constraint satisfaction.
  • Docstrings are updated.

TODO:
-Collision Free and Bounds Constraints. This will be updated after #266 is merged.
-Incorrect Constraint Validation.
@mkoval suggested

The constraint validation currently discretizes the trajectory using an integration time resolution. This is incorrect because integration time is meaningless - we should be collision checking at a fixed DOF resolution.

This will be updated after #212 is merged. The current solution is adding a user-defined check resolution in followVectorField().

using aikido::constraint::createTestableBounds;
auto boundConstraint = createTestableBounds(mMetaSkeletonStateSpace);
for (double t = trajectory.getStartTime(); t <= trajectory.getEndTime();
t += evalStepSize)
Copy link
Member

Choose a reason for hiding this comment

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

Two issues:

  • This will not evaluate the endpoint of the trajectory if getEndTime() is not an integral multiple of evalStepTime.
  • This will reevaluate the trajectory from the start each call. This function should take the start time for t to avoid re-evaluating that part of the trajectory.

for (double t = trajectory.getStartTime(); t <= trajectory.getEndTime();
t += evalStepSize)
{
auto state = mMetaSkeletonStateSpace->createState();
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 suggest moving this outside the loop, so we don't do unnecessary state allocations.

if (!constraint->isSatisfied(state))
{
throw ConstraintViolatedError();
}
Copy link
Member

Choose a reason for hiding this comment

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

See my previous comments. boundConstraint and constraint should be merged. The user can pass a CompoundTestable if they wish to check more than one constraint.


namespace aikido {
namespace planner {
namespace vectorfield {

//==============================================================================
double MoveEndEffectorOffsetVectorField::InvalidMaxDistance
= std::numeric_limits<double>::infinity();
Copy link
Member

Choose a reason for hiding this comment

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

infinity() seems like a perfectly valid value for the maximum distance if there is no limit. We commonly use this in PrPy when planning "move-until-touch" actions. I suggest using a negative value instead.

Eigen::Vector6d desiredTwist = computeGeodesicTwist(pose, mStartPose);
desiredTwist.tail<3>() = mDirection * mLinearVelocityGain;
cartesianVelocity = desiredTwist;
return 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 seems quite a bit different than the logic that it replaced. A few key differences: (1) the linear proportional controller is not additive and (2) there is no angular proportional controller. Were these changes intended?

///
/// \param[in] fromTrans Current transformation.
/// \param[in] toTrans Goal transformation.
/// \return Geodesic twist.
Copy link
Member

Choose a reason for hiding this comment

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

Document the convention about how to interpret the elements of this vector. Which elements represent linear versus angular velocities? What coordinate frame is this expressed in?

///
/// \param[in] fromTrans Current transformation.
/// \param[in] toTrans Goal transformation.
/// \return Geodesic error.
Copy link
Member

Choose a reason for hiding this comment

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

Document the convention about how to interpret the elements of this vector. Which elements represent linear versus angular velocities? What coordinate frame is this expressed in?

/// \param[in] fromTrans Current transformation.
/// \param[in] toTrans Goal transformation.
/// \param[in] r In units of meters/radians converts radians to meters.
/// \return Geodesic distance.
Copy link
Member

Choose a reason for hiding this comment

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

What are the units on the return value?

/// \param[in] jointVelocityUpperLimits Joint velocity upper bounds.
/// \param[in] jointVelocityLimited Whether joint velocity bounds are
/// considered in optimization.
/// \param[in] stepSize Step size in second.
Copy link
Member

Choose a reason for hiding this comment

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

What is this used for? Can we remove it?

/// \param[in] jointLimitPadding If less then this distance to joint
/// limit, velocity is bounded in that direction to 0.
/// \param[in] jointVelocityLowerLimits Joint velocity lower bounds.
/// \param[in] jointVelocityUpperLimits Joint velocity upper bounds.
Copy link
Member

Choose a reason for hiding this comment

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

These arguments should only be required if jointVelocityLimited is true. Are they ignored otherwise?

Instead of having a jointVelocityLimitReached flag, consider passing those arguments by const * and ignoring the velocity limits if they are nullptr.

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. Very nice work @dqyi11 - thank you for putting up with all of my comments. Bonus points for updating the docstrings - they help a lot when trying to understand how all the pieces fit together.

At this point, all of the major issues have been addressed. I left a handful of minor comments, most of which just just involve documenting limitations of the current implementation, so we remember to come back and fix them later.

Once those comments are addressed, I am happy to merge this. 👍

/// Evaluate Cartesian velocity by current pose of body node.
///
/// \param[in] pose Current pose of body node.
/// \param[out] cartesianVelocity Cartesian velocity defined in SE(3).
Copy link
Member

Choose a reason for hiding this comment

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

Pedantic Nit: Velocities live in se(3), not SE(3).

/// could be found.
/// \param[in] positionTolerance Constraint tolerance in meters.
/// \param[in] angularTolerance Constraint tolerance in radians.
/// \param[in] initialStepSize Initial step size.
Copy link
Member

Choose a reason for hiding this comment

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

Clarify that this is the maximum step size used to guarantee that the integrator does not step out of joint limits, not the initial step size.

double r,
double linearVelocityGain,
double angularVelocityGain,
double initialStepSize,
Copy link
Member

Choose a reason for hiding this comment

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

See my comment on BodyNodePoseVectorField. This is a confusing parameter.


/// Angular velocit gain.
double mAngularVelocityGain;
};
Copy link
Member

Choose a reason for hiding this comment

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

This object contains a Isometry3d member variable, which is a fixed-size vectorizable type. Therefore, we should flag this class with EIGEN_MAKE_ALIGNED_OPERATOR_NEW.


protected:
/// Goal pose.
Eigen::Isometry3d mGoalPose;
Copy link
Member

Choose a reason for hiding this comment

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

This object contains a Isometry3d member variable, which is a fixed-size vectorizable type. Therefore, we should flag this class with EIGEN_MAKE_ALIGNED_OPERATOR_NEW.

{
throw VectorFieldTerminated("State not in constraint.");
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This constraint validation logic is correct, but will result in a lot of extra constraint validation checks if the the integration step size is significantly smaller than the collision checking resolution. This is common if the robot is near a singularity because the ODE is relatively stiff.

A better way to implement this is to maintain the time mLastEvaluation at which the last constraint validation check was performed. In this function, we could collision check the trajectory from mLastEvaluation to t excluding the endpoint, then update mLastEvaluation = t. Finally, when planning terminates, we need to check the unevaluated portion [mLastEvaluation, getEndTime()] of the trajectory.

I think this is fine for now - this doesn't affect correctness. If you don't want to make this change now, then just file an issue so it doesn't slip through the cracks. 😉

trajEvalStartTime = mConstraintCheckResolution;
}
if (!mVectorField->evaluateTrajectory(
*trajSegment.get(),
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 is equivalent to *trajSegment.

}
}
mKnots.push_back(knot);
mIndex += 1;
Copy link
Member

Choose a reason for hiding this comment

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

It seems like mIndex always equals mKnots.size(). Is the variable necessary? 🤔

/// distance.
/// \param[in] linearVelocityGain Linear velocity gain in workspace.
/// \param[in] angularVelocityGain Angular velocity gain in workspace.
/// \param[in] initialStepSize Initial step size.
Copy link
Member

Choose a reason for hiding this comment

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

Clarify that this is the maximum step size used to guarantee that the integrator does not step out of joint limits, not the initial step size.

mConstraintCheckResolution,
trajEvalStartTime))
{
throw VectorFieldTerminated("State not in constraint.");
Copy link
Member

Choose a reason for hiding this comment

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

This should be a ConstraintViolatedError.

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 - this is a huge improvement over the vector field planner that currently lives in master. Thank you for your patience addressing all of the comments @dqyi11!

There are three follow-up issues from this pull request that we should address at some point: #274, #275, and #285.

/// \param[in] maxStepSize The maximum step size used to guarantee
/// that the integrator does not step out of joint limits.
/// \param[in] jointLimitpadding If less then this distance to joint
/// limit, velocity is bounded in that direction to 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: Missing docstring for the enforceJointVelocityLimits flag. I'd also suggest we make that flag default to false, since we typically use the vector field planner as a kinematic planner.

/// BodyNode
dart::dynamics::BodyNodePtr mBodyNode;

/// Initial step size of integrator.
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 comment should be updated to reflect the fact that this is a maximum step size.

return result;
Eigen::Vector6d desiredTwist = computeGeodesicTwist(pose, mStartPose);
desiredTwist.tail<3>() = mDirection;
cartesianVelocity = desiredTwist;
Copy link
Member

Choose a reason for hiding this comment

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

Yes, that makes sense. 👍 Thanks for the explanation.

throw std::invalid_argument(ss.str());
}
if (skeleton->getVelocityLowerLimit(i) > skeleton->getVelocityUpperLimit(i))
// dtwarn << e.what() << std::endl;
Copy link
Member

Choose a reason for hiding this comment

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

Remove dead code.

@dqyi11 dqyi11 merged commit 2a5aa04 into master Dec 10, 2017
@jslee02 jslee02 mentioned this pull request Jan 6, 2018
3 tasks
gilwoolee pushed a commit that referenced this pull request Jan 21, 2019
* new vector field planner and added move end effector pose vector field

* add move to end effector pose test cases

* refactor code and format

* add new tests to plan to end effector pose

* add functions of enabling collision checking and dof limit checking

* fix doc strings

* add velocity gain to support controlling in complex scenarios

* add new class for collision error

* address JS's comments

* address JS's comments and add doc strings for some member variables

* Fix const-correctness in ConfigurationSpaceVectorField

* Fix code style

* fix doc string style

* Add invalid max distance variable

* update cpde style

* fix typos

* address Mike's comments

* add parameter for constraint check resolution

* add doc strings, moved implementation details, add plannerStatus as output

* address Mike's comments

* update for formatting

* address Mike's comments

* address Mike's comments

* change to use TestableIntersection

* fix doc strings

* add changelog.md
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.

4 participants