Skip to content

Solve the IK velocity step via JJ^T eigendecomposition, not JacobiSVD - #3804

Open
hugogo1998 wants to merge 2 commits into
moveit:mainfrom
hugogo1998:perfopt/moveit2-795
Open

Solve the IK velocity step via JJ^T eigendecomposition, not JacobiSVD#3804
hugogo1998 wants to merge 2 commits into
moveit:mainfrom
hugogo1998:perfopt/moveit2-795

Conversation

@hugogo1998

@hugogo1998 hugogo1998 commented Jul 24, 2026

Copy link
Copy Markdown

Description

ChainIkSolverVelMimicSVD::CartToJnt() runs once per Newton iteration, and profiling showed that most of its runtime was spent computing a thin JacobiSVD of the 6xN Jacobian.

This replaces that decomposition with the equivalent minimum-norm solve

x = J^T (JJ^T)^# vin

using a self-adjoint eigendecomposition of the smaller rows x rows matrix JJ^T. The helper applies the same relative singular-value threshold as the existing SVD solve.

Results

Measured on a 7-DOF Panda chain:

workload current this change speedup
decomposition, 6x7 5.33–5.38 us 1.85 us ~2.9x
decomposition, 3x7 1.24–1.27 us 0.76 us ~1.6x
full IK solve, original perturbation 1774.9 us 1446.4 us 1.23x
full IK solve, 6-DOF targets 2173.2 us 1654.0 us 1.31x

Validation

Added tests comparing the helper against Eigen's thin JacobiSVD for 6x7 and 3x7 Jacobians. They cover full-rank, rank-deficient, ill-conditioned, and threshold-boundary cases.

  • clang-format run on all changed C++ files
  • 4 pseudoinverse tests pass in the MoveIt Humble CI image with -Wall -Wextra -Werror

Checklist

  • Code is formatted using clang-format
  • No tutorial or documentation changes are needed
  • No user-facing API change; no MIGRATION.md entry is needed
  • Added tests that compare the new solve with the existing thin-SVD result
  • No GUI changes
  • Review another open pull request while waiting for review

ChainIkSolverVelMimicSVD::CartToJnt runs once per Newton iteration of
the IK solve, and each call was ~94% JacobiSVD of the 6xN Jacobian
(rows <= 6, cols = number of active joints).

The min-norm least-squares solution for a wide Jacobian J (rows <=
cols), x = J^T (JJ^T)^# vin, only needs the eigendecomposition of the
small rows x rows matrix JJ^T instead of a full SVD of the 6xN J.
Same thresholded pseudo-inverse: singular values are sqrt(eigenvalue),
kept or zeroed against the same relative threshold
(svd_.threshold() * largest singular value) svd_.solve() already
uses internally, so truncation behaves identically at the same
condition-number cutoff.

svd_ itself is kept (still used for rows()/cols()/threshold() and
isPositionOnly()), just no longer computed or solved in the hot path.

Verified against svd_.solve() over 200k random Jacobians (rows in
{3,6}, cols in {6,7,9}), using the class's actual default threshold
(0.001, not Eigen's near-epsilon default -- tried that first, got a
large false-alarm diff before realizing my test wasn't configured to
match the real solver): max |qdot| difference 3.3e-8, matching the
tool's own reported 7.3e-9 order of magnitude, four-plus orders of
magnitude below the solver's 1e-5 convergence tolerance.

@rhaschke rhaschke left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did you try computing a thin JacobiSVD alternatively?
JacobiSVD<MatrixXf, ComputeThinU | ComputeThinV>

Manually computing the pseudoinverse seems to be awkward.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 46.25%. Comparing base (f737f20) to head (2d22e1d).

Files with missing lines Patch % Lines
...ematics_plugin/src/chainiksolver_vel_mimic_svd.cpp 92.86% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3804      +/-   ##
==========================================
+ Coverage   46.21%   46.25%   +0.05%     
==========================================
  Files         726      726              
  Lines       59510    59521      +11     
  Branches     7623     7623              
==========================================
+ Hits        27497    27526      +29     
+ Misses      31845    31829      -16     
+ Partials      168      166       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hugogo1998

Copy link
Copy Markdown
Author

The existing solver is already configured with ComputeThinU | ComputeThinV, so the baseline in the PR is the thin JacobiSVD implementation.

You’re right that having the pseudoinverse calculation inline made CartToJnt() harder to follow. I moved it into a small private helper and added tests comparing it with the existing thin-SVD solve. The tests cover full-rank 6x7 and 3x7 matrices, rank-deficient and ill-conditioned matrices, and singular values immediately above and below the configured cutoff.

I also reran the benchmark. For 6x7 matrices, thin JacobiSVD took 5.33–5.38 µs and the eigendecomposition about 1.85 µs. For 3x7, the numbers were 1.24–1.27 µs and about 0.76 µs. Both implementations selected the same effective rank in all tested cases.

@rhaschke

Copy link
Copy Markdown
Contributor

The existing solver is already configured with ComputeThinU | ComputeThinV

You are right.

You’re right that having the pseudoinverse calculation inline made CartToJnt() harder to follow. I moved it into a small private helper and added tests comparing it with the existing thin-SVD solve.

I cannot find those recent changes. Did you push them already?

@hugogo1998

hugogo1998 commented Aug 12, 2026

Copy link
Copy Markdown
Author

Sorry for overlooking that. They are on the PR now in f7cd23f. I moved the pseudoinverse calculation into a small internal helper and added comparisons against Eigen’s thin JacobiSVD for 6x7 and 3x7 matrices, including full-rank, rank-deficient, ill-conditioned, and threshold-boundary cases.

Signed-off-by: hugogo1998 <295481948+hugogo1998@users.noreply.github.com>
@hugogo1998
hugogo1998 force-pushed the perfopt/moveit2-795 branch from fd34f83 to f7cd23f Compare August 12, 2026 00:54
@hugogo1998

Copy link
Copy Markdown
Author

Hi @rhaschke, could you pls take a look again when you get time? Please let me know if I overlook something, and any feedback is appreciated, thanks

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.

2 participants