-
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
Refactor Parabolic timer and add Parabolic smoother #206
Conversation
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. I have a bunch of relatively minor comments that need to be addressed, but no major changes.
Having two functions for each operation - one for a Spline
and one for an Interpolated
- adds a lot of boilerplate. Could we eliminate the ones that accept Intnerpolated
and rely on the user to call the appropriate conversion function?
/// \return smoothed trajectory that satisfies acceleration constraints | ||
std::unique_ptr<trajectory::Spline> doShortcutAndBlend( | ||
const trajectory::Interpolated& _inputTrajectory, | ||
const aikido::constraint::TestablePtr _feasibilityCheck, |
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: Don't mark arguments that are passed by value in a function declaration as const
.
/// \param _feasibilityCheck Check whether a position is feasible | ||
/// \param _maxVelocity maximum velocity for each dimension | ||
/// \param _maxAcceleration maximum acceleration for each dimension | ||
/// \param _timelimit The maximum time to allow for doing shortcut |
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: Specify the units (seconds, I think).
/// \param _maxAcceleration maximum acceleration for each dimension | ||
/// \param _timelimit The maximum time to allow for doing shortcut | ||
/// \param _useVelocity whether velocity is considered in shortcut | ||
/// \param _blendRadius the radius used in doing blend |
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: Specify the units (seconds, I think).
/// \return smoothed trajectory that satisfies acceleration constraints | ||
std::unique_ptr<trajectory::Spline> doBlend( | ||
const trajectory::Interpolated& _inputTrajectory, | ||
const aikido::constraint::TestablePtr _feasibilityCheck, |
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's meaningless to mark an argument that is passed by value as const
in a function declaration.
/// \param _useVelocity whether velocity is considered in shortcut | ||
/// \return smoothed trajectory that satisfies acceleration constraints | ||
std::unique_ptr<trajectory::Spline> doShortcut( | ||
const trajectory::Spline* _inputTrajectory, |
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: Take the _inputTrajectory
by const &
instead of const *
.
} | ||
} | ||
|
||
TEST_F(ParabolicSmootherTests, doShort) |
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.
Typo: doShortcut
.
state.setValue(p4); | ||
nonStraightLine.addWaypoint(0.75, state); | ||
state.setValue(p5); | ||
nonStraightLine.addWaypoint(1., state); |
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: Could we implement this test using only three 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.
No.... In Hauser's code, shortcut will be executed only when the waypoint number is larger than 3....
smoothedTrajectory->evaluate(smoothedTrajectory->getEndTime(), state); | ||
EXPECT_TRUE(p5.isApprox(state.getValue())); | ||
|
||
EXPECT_TRUE(shortenDist <= orignDist); |
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: Make this a strict inequality and increase the time limit until it succeeds with very high probability.
|
||
double originTime = nonStraightLine.getDuration(); | ||
double shortenTime = smoothedTrajectory->getDuration(); | ||
EXPECT_TRUE(shortenTime<=originTime); |
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.
Make this a strict inequality.
|
||
double originTime = nonStraightLine.getDuration(); | ||
double shortenTime = smoothedTrajectory->getDuration(); | ||
EXPECT_TRUE(shortenTime<=originTime); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment above. Make this a strict inequality.
Also: There's a lot of duplicate code here. Can we move some of it to a helper function?
auto outputPath = make_unique<ParabolicRamp::DynamicPath>(); | ||
// Apply the adjoint limits | ||
outputPath->Init(toVector(_maxVelocity), toVector(_maxAcceleration)); | ||
outputPath->SetMilestones(milestones); |
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.
We need another version of this helper function - or an overload of this function - that optionally passes velocities to SetMilestones
. We need that for all of the functions that accept non-piecewise linear splines.
|
||
double startTime = 0.0; | ||
auto dynamicPath = convertToDynamicPath(_inputTrajectory, startTime, | ||
_maxVelocity, _maxAcceleration); |
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 call to convertToDynamicPath
should preserve velocities.
_blendIterations); | ||
double startTime = 0.0; | ||
auto dynamicPath = convertToDynamicPath(_inputTrajectory, startTime, | ||
_maxVelocity, _maxAcceleration); |
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 call to convertToDynamicPath
should preserve velocities.
|
||
double startTime = 0.0; | ||
auto dynamicPath = convertToDynamicPath(_inputTrajectory, startTime, | ||
_maxVelocity, _maxAcceleration); |
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 call to convertToDynamicPath
should preserve velocities.
@mkoval All the comments are addressed. |
stateSpace->expMap(eigA, startState); | ||
stateSpace->expMap(eigB, goalState); | ||
|
||
aikido::util::VanDerCorput vdc{1, true, true, checkResolution_}; |
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.
/// | ||
/// while _timelimit is not out: | ||
/// - Randomly sample two times uniformly at random. | ||
/// - Find two waypoints according two times in _inputTrajectory. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is technically incorrect. We evaluate the trajectory at whatever times we sample, which are almost surely (in the technical sense of that term) not 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.
@mkoval Should I then say "points" instead of "waypoints"?
/// - Randomly sample two times uniformly at random. | ||
/// - Find two waypoints according two times in _inputTrajectory. | ||
/// - Attempt to connect the two waypoints with a time-optimal parabolic | ||
/// spline. |
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 note that we then evaluate the feasibility of the spline using _feasibilityCheck
.
const Eigen::VectorXd& _maxAcceleration, | ||
double _timelimit = DEFAULT_TIMELIMT, | ||
double _tolerance = DEFAULT_TOLERANCE, | ||
aikido::util::RNG::result_type _rngSeed = std::random_device{}()); |
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 is no need to take a seed here - we can simply pass in a util::RNG&
to use for sampling.
/// | ||
/// This function smooths `_inputTrajectory' by firstly applying shortcut | ||
/// to _inputTrajectory and then using blend to remove segments that have | ||
/// zero velocities. |
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.
Note that this function is equivalent to calling doShortcut
, followed bydoBlend
. Add a note that this function is more efficient than calling those two functions in sequence because it avoids duplicated effort.
double _blendRadius = DEFAULT_BLEND_RADIUS, | ||
int _blendIterations = DEFAULT_BLEND_ITERATIONS, | ||
double _tolerance = DEFAULT_TOLERANCE, | ||
aikido::util::RNG::result_type _rngSeed = std::random_device{}() |
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 should be a util::RNG&
, not a seed. See my note above.
/// \param[out] position at time \c _t | ||
/// \param[out] velocity at time \c _t | ||
void evaluateAtTime(ParabolicRamp::DynamicPath& _path, double _t, | ||
Eigen::VectorXd& _position, Eigen::VectorXd& _velocity); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be safe to take _path
by const &
here.
convertToDynamicPath(const aikido::trajectory::Spline& _inputTrajectory, | ||
const Eigen::VectorXd& _maxVelocity, | ||
const Eigen::VectorXd& _maxAcceleration, | ||
bool _useVelocity = true); |
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 suggest removing the default value for _useVelocity
because it dramatically changes the behavior of the function. Or - if it does not require duplicating too much code - split this into two separate functions.
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.
@dqyi11 I would still like to address this.
src/trajectory/Interpolated.cpp
Outdated
mInterpolator->getDerivative(mWaypoints[0].state, mWaypoints[1].state, | ||
_derivative, alpha, _tangentVector); | ||
_tangentVector /= segmentTime; | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see - that makes sense if we have to handle this case. I am inclined to remove handling of this case for now, since an Interpolated
trajectory with a GeodesicInterpolator
has undefined derivatives at its waypoints.
src/trajectory/Interpolated.cpp
Outdated
{ | ||
if (_index < mWaypoints.size()) | ||
//return getStartTime() + mWaypoints[_index].t; | ||
return mWaypoints[_index].t; |
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 see - that seems to be correct. 👍
|
||
//============================================================================= | ||
double Spline::getWaypointTime(size_t _index) 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.
Check that _index < mSegments.size()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is resolved.
I have addressed all the comments. The only thing left is whether we shall keep "useVelocity" in converting an interpolated to a spline. In that, I convert a spline trajectory to a dynamic path without providing velocity. |
|
||
/// Convert an interpolated trajectory to a spline trajectory | ||
/// This function requires the \c _inputTrajectory to use a \c GeodesicInterpolator. | ||
/// This function requires the \c _inputTrajectory to use a \c | ||
/// GeodesicInterpolator. |
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: Could you fix the wrapping? ClangFormat isn't good at it. 😞
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 thought "So the conversion ..." was the continuing sentence without line breaking.
{ | ||
} | ||
|
||
virtual bool ConfigFeasible(ParabolicRamp::Vector const& x) |
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: Please use const X&
pattern rather than X 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.
Also, please begin with a lower letter for member functions.
, statespace_(testable_->getStateSpace()) | ||
, interpolator_(statespace_) | ||
{ | ||
} |
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: Please add // Do nothing
comment to indicate that this function is intentionally empty (but initializing the member variables).
} | ||
|
||
virtual bool SegmentFeasible( | ||
ParabolicRamp::Vector const& a, ParabolicRamp::Vector const& b) |
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: Ditto
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.
Please rename to segmentFeasible(...)
.
double checkResolution_; | ||
aikido::statespace::StateSpacePtr statespace_; | ||
aikido::statespace::GeodesicInterpolator interpolator_; | ||
aikido::constraint::TestablePtr testable_; |
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: Could you rename these private member variables using the pattern of mX
? I believe we don't use the trailing underscore for member variables (even for private member variables).
// that two waypoints are closer than _blendRadius together - which means | ||
// that waypoint indicies can change between iterations of the algorithm. | ||
size_t const numRamps = dynamicPath.ramps.size(); | ||
double const tMax = dynamicPath.GetTotalTime(); |
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 double
rampNd.blendAttempts++; | ||
} | ||
} | ||
double const t1 = std::max(t - dtShortcut, -ParabolicRamp::EpsilonT); |
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 double
} | ||
} | ||
double const t1 = std::max(t - dtShortcut, -ParabolicRamp::EpsilonT); | ||
double const t2 |
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 double
double const t2 | ||
= std::min(t + dtShortcut, tMax + ParabolicRamp::EpsilonT); | ||
|
||
bool const success = dynamicPath.TryShortcut(t1, t2, feasibilityChecker); |
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 bool
for (int attempt = 0; attempt < blendIterations; ++attempt) | ||
{ | ||
while (tryBlend(dynamicPath, feasibilityChecker, attempt, dtShortcut)) | ||
; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😱 What's happening 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.
Nit: At first, I thought this was a mistake, but now I got it. I would like to suggest make this code easier to read something like:
bool success;
do
{
success = tryBlend(...);
} while (success);
Also, I'm not sure which one is logically correct between while(success)
and while(!success)
. 😕
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 current logic is correct. The core issue is that blending can completely remove waypoints from the trajectory, which makes bookkeeping via the time parameter extraordinarily difficult. We dodge that issue by allowing tryBlend
to always start at the beginning of the trajectory. We continue to call tryBlend
in a loop until it finds nothing left to blend.
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.
That makes sense. It would be nice to add this as a comment at least to tryBlend()
.
I still prefer to change the code to be self-documenting. For example:
bool needMoreBlending;
do
{
needMoreBlending = tryBlend(...);
} while (needMoreBlending);
…personalrobotics/aikido into feature/ParabolicSmootherHelper
/// | ||
/// This function smooths `_inputTrajectory' by iteratively sampling | ||
/// two waypoints in a trajectory and trying to find a shortcut using | ||
/// the following algorithm: |
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: "waypoints" -> "points".
aikido::constraint::TestablePtr _feasibilityCheck, | ||
const Eigen::VectorXd& _maxVelocity, | ||
const Eigen::VectorXd& _maxAcceleration, | ||
aikido::util::RNG& _rngSeed, |
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 is the RNG, not a seed for the RNG. 😉
mStateSpace->expMap(eigA, startState); | ||
mStateSpace->expMap(eigB, goalState); | ||
|
||
aikido::util::VanDerCorput vdc{1, false, false, mCheckResolution}; |
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: Add a comment explaining why includeStartpoint
and includeEndpoint
can be false
.
aikido::util::RNG& rng) | ||
{ | ||
if (timelimit < 0.0) | ||
throw std::runtime_error("Timelimit should be non-negative"); |
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:
- Add a bounds check for
checkResolution
. - Use
std::invalid_argument
instead ofstd::runtime_error
for these exceptions.
SmootherFeasibilityCheckerBase base(testable, checkResolution); | ||
ParabolicRamp::RampFeasibilityChecker feasibilityChecker(&base, tolerance); | ||
|
||
std::chrono::time_point<std::chrono::system_clock> startTime, currentTime; |
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:
currentTime
is only used inside the loop, so move its definition to where it is first used.- Initialize
startTime
on the same line where the variable it is defined.
protected: | ||
void SetUp() override | ||
{ | ||
mRng = aikido::util::RNGWrapper<std::mt19937>( std::random_device{}() ); |
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.
Please use a deterministic seed (e.g. 0
) in unit tests so their result is not stochastic.
state.setValue(Vector2d(3., 4.)); | ||
mStraightLine->addWaypoint(1., state); | ||
|
||
mNonStraightLineLength = initNonStraightLine(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's odd to have one output (mNonStraightLine
) in a member variable and a second mNonStraightLineLength
as a return value. Please stick to one convention or the other for this function.
auto currState = mStateSpace->createState(); | ||
auto nextState = mStateSpace->createState(); | ||
Eigen::VectorXd currVec, nextVec; | ||
for(size_t i=0; i<spline->getNumWaypoints()-1;++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: This will overflow if getNumWaypoints() == 0
, which could cause the test to SEGFAULT instead of fail properly. Please refactor the loop so that is not an issue, e.g. by starting with i = 1
and replacing i
with i - 1
in the body.
|
||
TEST_F(ParabolicSmootherTests, convertInterpolatedToSpline) | ||
{ | ||
auto spline = convertToSpline(*mStraightLine.get()); |
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: *mStraightLine.get()
is equivalent to *mStraightLine
.
mStraightLine->evaluateDerivative(t, 1, interpolatedTangent); | ||
EXPECT_EIGEN_EQUAL(splineTangent, interpolatedTangent, mTolerance); | ||
} | ||
} |
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 tests for:
- More than one segment, e.g. the non-striaghtline trajectory.
- A trajectory with a non-zero start time.
Codecov Report
@@ Coverage Diff @@
## master #206 +/- ##
=========================================
Coverage ? 70.79%
=========================================
Files ? 168
Lines ? 4897
Branches ? 774
=========================================
Hits ? 3467
Misses ? 954
Partials ? 476
|
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.
Looks good to me. Great work @dqyi11!
|
||
// tryBlend always starts at the beginning of the trajectory. | ||
// Without this bookkeeping, tryBlend would could any blend that fails | ||
// multiple times. |
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: Typo "would could." I think this should say "tryBlend would retry any blend that fails multiple times."
{ | ||
noMoreBlending | ||
= tryBlend(dynamicPath, feasibilityChecker, attempt, dtShortcut); | ||
} while (noMoreBlending); |
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 noMoreBlending
implies the opposite of what it actually is. I suggest renaming the flag to needsMoreBlending
.
else if (dynamic_cast<const R<6>*>(_stateSpace) != nullptr) | ||
{ | ||
return true; | ||
} |
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.
Resolved.
milestones.emplace_back(toVector(currVec)); | ||
|
||
_inputTrajectory.getWaypointDerivative(iwaypoint, 1, tangentVector); | ||
velocities.emplace_back(toVector(tangentVector)); |
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.
Resolved by splitting the Spline
and Interpolated
cases into separate overloads. 👍
(1) Refactor Parabolic timer into
(1.a) convert an interpolated into a spline
(1.b) convert a spline into a timed spline
(2) Add Parabolic smoother
(2.a) convert a spline to a dynamic path
(2.b) convert a dynamic path to a spine
(2.c) add HauserParabolicSmoother class that handles "doShortcut", "doBlend" and "doShortcutAndBlend"