Return identity directly when resolving a frame against itself - #1693
Conversation
resolvePose computes the pose of a frame relative to a resolve-to frame as poseR.Inverse() * _pose. When the two frames are the same vertex this composes an edge chain with its own inverse, which is the identity in real arithmetic but not necessarily in floating point: the quaternion round trip can leave a residual on the order of 2^-56. Return the identity directly for that case instead of deriving it. The first resolvePoseRelativeToRoot call is kept so the existing validation and error diagnostics for the frame vertex are unchanged. Adds a test pinning the contract, both at the resolvePose level and through JointAxis::ResolveXyz for an axis declared without xyz_expressed_in. Part of gazebosim#1692 Assisted-by: Claude Opus 5 (Anthropic Claude Code) Signed-off-by: samirbhattarai135 <147074565+samirbhattarai135@users.noreply.github.com>
I manually reverted the changes to FrameSemantics.cc from this branch and then build and ran the new test, and it fails locally for me on a macOS arm64 machine: so I don't know why you weren't able to reproduce the problem (maybe testing with amd64 instead of arm64?), but this looks good to me |
|
Thank you for reverting and running it — that resolves it, and it means my "could not reproduce" note in #1692 was wrong. I have corrected the PR description. It was not amd64, which makes it more interesting. My runs used sdformat's existing So same architecture as your machine, same source, opposite result. The remaining difference is the toolchain — the Apple clang on the macOS 26 image evidently contracts the quaternion multiply so the round trip cancels exactly, while yours does not. That also fits the original amd64-passes / arm64-fails split in gazebosim/gz-sim#3602 better than architecture alone does: it looks like FP contraction in codegen rather than anything about the ISA. Practical consequence worth flagging: this will not reproduce on GitHub's I will correct my earlier comments on #1692 and gazebosim/gz-sim#3602 accordingly — with the residual confirmed real in I have also pushed a benchmark on a separate branch comparing current |
|
@osrf-jenkins run tests please |
The test iterated string literals through const std::string &, which binds each literal to a temporary constructed for the iteration. GCC 15 reports this as -Wrange-loop-construct, and the Jenkins jobs gate on compiler warnings, so the resolute-amd64 run went unstable with all tests passing. Let the loop variable deduce instead, matching the existing idiom in parser.cc. The literals still convert to std::string at each call site. Assisted-by: Claude Opus 5 (Anthropic Claude Code) Signed-off-by: samirbhattarai135 <147074565+samirbhattarai135@users.noreply.github.com>
|
@scpeters heads up on the red Both warnings were mine, in the new test: I bound string literals to Fixed in 3081c36 by letting the loop variable deduce, matching the existing idiom at |
|
thanks for the contribution! |
|
https://github.com/Mergifyio backport sdf16 |
✅ Backports have been createdDetails
|
The sdformat bug that caused the arm64 failure is fixed by gazebosim/sdformat#1693, so the expectations can be restored on arm64. This reverts commit e6a7439. Assisted-by: Claude Opus 5 (Anthropic Claude Code) Signed-off-by: samirbhattarai135 <147074565+samirbhattarai135@users.noreply.github.com>
resolvePose computes the pose of a frame relative to a resolve-to frame as poseR.Inverse() * _pose. When the two frames are the same vertex this composes an edge chain with its own inverse, which is the identity in real arithmetic but not necessarily in floating point: the quaternion round trip can leave a residual on the order of 2^-56. Return the identity directly for that case instead of deriving it. The first resolvePoseRelativeToRoot call is kept so the existing validation and error diagnostics for the frame vertex are unchanged. Adds a test pinning the contract, both at the resolvePose level and through JointAxis::ResolveXyz for an axis declared without xyz_expressed_in. Part of #1692 Assisted-by: Claude Opus 5 (Anthropic Claude Code) Signed-off-by: samirbhattarai135 <147074565+samirbhattarai135@users.noreply.github.com> Co-authored-by: Steve Peters <scpeters@intrinsic.ai> (cherry picked from commit d08e88d)
|
https://github.com/Mergifyio backport sdf15 |
✅ Backports have been createdDetails
|
resolvePose computes the pose of a frame relative to a resolve-to frame as poseR.Inverse() * _pose. When the two frames are the same vertex this composes an edge chain with its own inverse, which is the identity in real arithmetic but not necessarily in floating point: the quaternion round trip can leave a residual on the order of 2^-56. Return the identity directly for that case instead of deriving it. The first resolvePoseRelativeToRoot call is kept so the existing validation and error diagnostics for the frame vertex are unchanged. Adds a test pinning the contract, both at the resolvePose level and through JointAxis::ResolveXyz for an axis declared without xyz_expressed_in. Part of #1692 Assisted-by: Claude Opus 5 (Anthropic Claude Code) Signed-off-by: samirbhattarai135 <147074565+samirbhattarai135@users.noreply.github.com> Co-authored-by: Steve Peters <scpeters@intrinsic.ai> (cherry picked from commit d08e88d)
|
https://github.com/Mergifyio backport sdf14 |
✅ Backports have been createdDetails
|
resolvePose computes the pose of a frame relative to a resolve-to frame as poseR.Inverse() * _pose. When the two frames are the same vertex this composes an edge chain with its own inverse, which is the identity in real arithmetic but not necessarily in floating point: the quaternion round trip can leave a residual on the order of 2^-56. Return the identity directly for that case instead of deriving it. The first resolvePoseRelativeToRoot call is kept so the existing validation and error diagnostics for the frame vertex are unchanged. Adds a test pinning the contract, both at the resolvePose level and through JointAxis::ResolveXyz for an axis declared without xyz_expressed_in. Part of #1692 Assisted-by: Claude Opus 5 (Anthropic Claude Code) Signed-off-by: samirbhattarai135 <147074565+samirbhattarai135@users.noreply.github.com> Co-authored-by: Steve Peters <scpeters@intrinsic.ai> (cherry picked from commit d08e88d)
|
https://github.com/Mergifyio backport sdf12 |
✅ Backports have been createdDetails
|
resolvePose computes the pose of a frame relative to a resolve-to frame as poseR.Inverse() * _pose. When the two frames are the same vertex this composes an edge chain with its own inverse, which is the identity in real arithmetic but not necessarily in floating point: the quaternion round trip can leave a residual on the order of 2^-56. Return the identity directly for that case instead of deriving it. The first resolvePoseRelativeToRoot call is kept so the existing validation and error diagnostics for the frame vertex are unchanged. Adds a test pinning the contract, both at the resolvePose level and through JointAxis::ResolveXyz for an axis declared without xyz_expressed_in. Part of #1692 Assisted-by: Claude Opus 5 (Anthropic Claude Code) Signed-off-by: samirbhattarai135 <147074565+samirbhattarai135@users.noreply.github.com> Co-authored-by: Steve Peters <scpeters@intrinsic.ai> (cherry picked from commit d08e88d)
🎉 New feature
Part of #1692
Summary
resolvePosecomputes the pose of a frame relative to a resolve-to frame asposeR.Inverse() * _pose. When both are the same vertex, that composes an edge chain with its own inverse — the identity in real arithmetic, but not guaranteed in floating point. This returns the identity directly for that case instead of deriving it.The first
resolvePoseRelativeToRootcall is kept, so validation and error diagnostics for the frame vertex are unchanged.What this changes
resolvePoseleaves a sub-epsilon residual when a frame is resolved against itself, and this removes it. Confirmed by @scpeters on macOS arm64 by reverting theFrameSemantics.ccchange and running the new test:That
xyz.Y()value is exactly what gazebosim/gz-sim#3602 prints for the joint axis.Correction to my earlier claim in this PR and in #1692. I originally wrote that the test passed with and without the change. That was wrong, and the reason is worth recording: my CI runs used GitHub's
macos-latest, which now resolves to themacos-26-arm64image. So the architecture was right, but the toolchain was not the one that exhibits the residual — the newer Apple clang on macOS 26 apparently contracts the quaternion multiply such that the round trip cancels exactly. Same source, same architecture, different codegen, opposite result.Worth knowing for anyone testing this area: reproducing it needs the right compiler, not just an arm64 machine, and CI on
macos-latestwill not show it.Test
FrameSemantics.resolveAgainstOwnFrameIsExactchecks the identity case at theresolvePoselevel and throughJointAxis::ResolveXyzfor an axis declared withoutxyz_expressed_in.EXPECT_EQcannot be used here:Pose3dandVector3dcompare through tolerance-basedoperator==, so a residual near1e-17compares equal. Components are checked directly.The fixture uses
-1.5707, mirroring gz-sim'sstatic_diff_drive_vehicle.sdf. Note thatmodel_frame_relative_to_joint.sdfuses only0and exactpi/2rotations and round trips exactly on some toolchains, so a test written against that fixture can pass whether or not the behaviour is present.Checklist
codecheckpassed (See contributing)I have not run the full suite locally — I do not have a working sdformat build on this machine, so I have been relying on the fork's CI. Flagging that rather than ticking the box.
Note: Assisted by Claude (Anthropic), per the GenAI disclosure policy; commits carry an
Assisted-by:trailer. All analysis and measurements above were verified against real CI runs rather than taken on trust.