Solve the IK velocity step via JJ^T eigendecomposition, not JacobiSVD - #3804
Open
agentperfopt wants to merge 1 commit into
Open
Solve the IK velocity step via JJ^T eigendecomposition, not JacobiSVD#3804agentperfopt wants to merge 1 commit into
agentperfopt wants to merge 1 commit into
Conversation
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
reviewed
Jul 24, 2026
rhaschke
left a comment
Contributor
There was a problem hiding this comment.
Did you try computing a thin JacobiSVD alternatively?
JacobiSVD<MatrixXf, ComputeThinU | ComputeThinV>
Manually computing the pseudoinverse seems to be awkward.
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Builds on #795 (a clock fix for timeout handling under simulated time; the IK math is unchanged).
ChainIkSolverVelMimicSVD::CartToJntruns once per Newton iteration of the IK solve, and profiling shows about 94% of its runtime is spent in theJacobiSVDof the 6xN Jacobian.The minimum-norm least-squares solution for a wide Jacobian
J(rows ≤ cols),x = J^T (JJ^T)^# vinonly requires the eigendecomposition of the small
rows×rowsmatrixJJ^T, rather than a full SVD of the6xNJacobian. Singular values are recovered assqrt(eigenvalue)and truncated using the same relative threshold (svd_.threshold() * largest singular value) thatsvd_.solve()already applies internally, so the conditioning cutoff is unchanged.svd_remains in place and is still used forrows(),cols(),threshold(), andisPositionOnly(); this change only replaces the decomposition in the hot path.Results
Real Orocos KDL, 7-DOF Panda chain. Per Newton iteration, decomposition only:
Per full IK solve:
Position-only IK sees little improvement because it converges in very few iterations and the 3xN decomposition is already inexpensive.
The per-iteration speedup is larger than the end-to-end improvement because a full solve also includes FK, convergence checks, and timeout-limited failures where both implementations spend essentially the same wall-clock time. One possible follow-up would be to reuse a
SelfAdjointEigenSolvermember instead of constructing one on every call, but I left that out since it's a larger design change than the optimization profiled here.Correctness
For direct (first-attempt) solves, the optimized implementation produces bit-identical results to the current solver across a sweep of real targets.
I also independently verified the pseudo-inverse implementation against 200k random Jacobians (
rows ∈ {3,6},cols ∈ {6,7,9}), using the solver's configured threshold (0.001) rather than Eigen's default threshold. I initially tested against Eigen's default and saw much larger differences, which turned out to be due to the mismatched truncation threshold rather than the implementation itself.With the correct threshold, the maximum
|qdot|difference was3.3e-8, over four orders of magnitude below the solver's1e-5convergence tolerance, so this does not affect convergence or the resulting solutions.Checklist
Found by an automated profiling tool that searches merged PRs for additional optimization opportunities.