Skip to content

Fix/delta limit rot6d displacement#153

Open
Tushar2604 wants to merge 3 commits into
robocurve:mainfrom
Tushar2604:fix/delta-limit-rot6d-displacement
Open

Fix/delta limit rot6d displacement#153
Tushar2604 wants to merge 3 commits into
robocurve:mainfrom
Tushar2604:fix/delta-limit-rot6d-displacement

Conversation

@Tushar2604

Copy link
Copy Markdown
Contributor

Fix unsafe rot6d displacement pose handling in DeltaLimitApprover

Summary

DeltaLimitApprover incorrectly allowed rot6d in displacement pose mode (eef_delta_pose).

Unlike translation deltas, a rot6d "no-op" is not the zero vector—it is (1, 0, 0, 0, 1, 0). Clamping each dimension independently toward a symmetric ±max_delta range distorts this representation. Since every downstream consumer re-normalizes rot6d using Gram-Schmidt, the distorted value can become an arbitrarily large rotation.

As a result, the delta limiter can amplify rotations instead of limiting them.

This is the same underlying failure mode as clamping absolute quaternions, and the same class of issue already addressed for quat_wxyz/quat_xyzw displacement poses in #144.

This is not a regression—the behavior existed before #143 and #144.


Changes

Validation

  • Added "rot6d" to _DISPLACEMENT_POSE_UNSAFE_ROT.
    • The set is now:
      {quat_wxyz, quat_xyzw, rot6d}
  • No additional logic was required.
    • The existing constructor validation and error message already handle all entries in this set.
  • rot6d remains supported for absolute orientation modes.
    • Only displacement (eef_delta_pose) is rejected.

Tests

  • Generalized the displacement validation test to accept (rotation_repr, rot_dim) pairs.
    • This allows testing both quaternion (4D) and rot6d (6D) action spaces.
  • Added a conformance test verifying that eef_delta_pose + rot6d now raises the expected guardrails error.
    • This mirrors the existing coverage for:
      • valid Euler displacement
      • invalid absolute quaternion

Documentation

  • Updated adapters.md to describe the guardrail based on the actual failure condition instead of listing individual rotation representations.
  • Added a breaking entry under Unreleased → Fixed in CHANGELOG.md.

Why this change?

rot6d cannot be safely limited using independent per-dimension clamping in displacement mode because:

  1. Its identity rotation is not the origin.
  2. Clamping distorts the representation.
  3. Gram-Schmidt normalization projects the distorted vector back onto SO(3).
  4. The resulting rotation can be much larger than the intended delta.

Rejecting this configuration is therefore the correct and safe behavior.


Checklist

  • Added unit and conformance tests
  • ruff check .
  • ruff format --check .
  • mypy (strict)
  • Full test suite passes
    • 730 tests
    • 100% coverage
  • Updated CHANGELOG.md
  • No public API changes
  • No new dependencies (NumPy-only)

Note: pre-commit run --all-files could not be executed locally because it shells out to uv, which was unavailable. The equivalent checks (ruff, mypy, pytest --cov) were run directly, along with the full Linux test suite.


Related

Closes #150

This branch is based on #144, since it builds directly on the _DISPLACEMENT_POSE_MODES and _DISPLACEMENT_POSE_UNSAFE_ROT infrastructure introduced there. It will be rebased onto main once #144 is merged.

DeltaLimitApprover rejected any pose mode whose rotation_repr was not in
{none, rot6d}, but that guard fired for displacement pose modes too. For
eef_delta_pose the per-dim quantities are small rotation deltas, which clamp
per dimension just like any bounded displacement; only absolute orientations
(eef_abs_pose) have the wraparound and axis-coupling problems the guard
targets.

Restrict the rotation-repr refusal to absolute pose modes. The displacement
review path already clamps every dim through the box, so no runtime change is
needed there. Downstream recovers for free: conformance's guardrail probe now
reports euler-delta embodiments conformant, and CLI runs keep delta limiting
instead of degrading to clamp-only.

Closes robocurve#143.
Address review: the displacement-pose branch had no rotation-repr gate at
all, so quat_wxyz/quat_xyzw slipped through alongside the intended
euler_xyz/axis_angle. A quaternion delta's identity is (1, 0, 0, 0), not
the zero vector, so clamping it per dimension toward a symmetric
±max_delta box drags it away from identity; downstream re-normalization
can then amplify the rotation instead of limiting it (same failure class
as clamping an absolute quaternion).

- Add _DISPLACEMENT_POSE_UNSAFE_ROT = {quat_wxyz, quat_xyzw} and a second
  constructor refusal, self-maintaining via _DISPLACEMENT_POSE_MODES =
  _POSE_MODES - _ABSOLUTE_POSE_MODES.
- Fix the quat test's shape bug (a quat delta pose is 3 pos + 4 quat + 1
  gripper = 8 dims, not 7) and split it into its own parametrized refusal
  test over quat_wxyz/quat_xyzw, covering quat_xyzw for the first time.
  euler_xyz/axis_angle keep the original happy-path clamp assertion.
- Drop mid-sentence bold in the docstring, adapters.md, and the CHANGELOG
  entry, and update their content to describe the quat refusal accurately.

rot6d has the same identity-not-at-origin problem in displacement mode but
is intentionally out of scope here (robocurve#150).
A rot6d delta's identity is (1, 0, 0, 0, 1, 0), not the zero vector, same
problem class as the quaternion case robocurve#144 fixed: per-dimension clamping
toward a symmetric ±max_delta box drags it away from identity, and the
Gram-Schmidt orthonormalization every consumer applies can then amplify
the rotation instead of limiting it.

Pre-existing behavior, not a regression: eef_delta_pose + rot6d already
reached the displacement clamp path before robocurve#143/robocurve#144.

- Add "rot6d" to _DISPLACEMENT_POSE_UNSAFE_ROT (now {quat_wxyz, quat_xyzw,
  rot6d}); the constructor refusal and its message already generalize, no
  change needed there. rot6d stays limitable for *absolute* orientations
  (_LIMITABLE_ROT is unrelated to _DISPLACEMENT_POSE_UNSAFE_ROT).
- Generalize the displacement-refusal test to parametrize over
  (rotation_repr, rot_dim) so rot6d's 6-dim rotation and quat's 4-dim
  rotation each get the correct action-space shape.
- Add a conformance test: a rot6d-delta embodiment is now a guardrails
  error, mirroring the existing euler-delta (conformant) and absolute-quat
  (error) tests.
- Update adapters.md's guardrails row and add a CHANGELOG entry noting the
  breaking behavior change for any embodiment currently declaring rot6d
  deltas.

Builds on the not-yet-merged robocurve#144 (same branch lineage); will rebase once
that lands.
@jeqcho jeqcho closed this Jul 21, 2026
@jeqcho jeqcho reopened this Jul 21, 2026
@jeqcho

jeqcho commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Reviewed alongside #144 and the rot6d reasoning holds up: the no-op at (1, 0, 0, 0, 1, 0) sits off-origin, so box-clamping toward zero distorts it and Gram-Schmidt downstream can turn that into an arbitrary rotation. Adding rot6d to _DISPLACEMENT_POSE_UNSAFE_ROT while keeping it clampable for absolute modes is exactly what #150 asked for, and the conformance tests in tests/test_conformance.py closing the loop through check_embodiment rather than stopping at constructor behavior is appreciated.

Since this branch carries #144's commits, the path to landing is: #144 rebases and merges first, then rebase this one onto main so the diff reduces to the rot6d commit. Right now both branches share the same one-hunk CHANGELOG.md conflict with main, and CI won't run on either until the conflict clears (GitHub skips pull_request workflows when it can't build the merge commit).

@Tushar2604

Copy link
Copy Markdown
Contributor Author

Understood on the sequencing — #144 is rebased and pushed (conflict-free now, force-pushed as 0611b14), so it's ready on your end. I won't rebase this branch until #144 actually lands on main, since rebasing onto my own unmerged branch right now wouldn't be the same as rebasing onto main, and could drift if anything changes during merge. Will rebase this one as soon as #144 is in.

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.

[bug] DeltaLimitApprover can amplify rot6d rotation deltas in displacement pose modes

2 participants