Skip to content

Bring in Sharpa retargeter from V2D#444

Closed
jiwenc-nv wants to merge 3 commits into
NVIDIA:mainfrom
jiwenc-nv:sharpa_with_v2d
Closed

Bring in Sharpa retargeter from V2D#444
jiwenc-nv wants to merge 3 commits into
NVIDIA:mainfrom
jiwenc-nv:sharpa_with_v2d

Conversation

@jiwenc-nv
Copy link
Copy Markdown
Collaborator

@jiwenc-nv jiwenc-nv commented Apr 30, 2026

Note: V2D is still pre-release and is gated from public access.

Summary by CodeRabbit

  • New Features

    • Added Sharpa hand retargeting capability with bimanual support for converting hand-tracking inputs to joint angles.
    • Introduced optional bundling of robotic dependencies into wheel builds via configuration flag.
  • Documentation

    • Added comprehensive setup and usage guide for Sharpa hand retargeting with code examples.
  • Tests

    • Added test suite validating Sharpa hand retargeting behavior and warm-start functionality.
  • Chores

    • Enhanced build system to support optional dependency bundling and artifact cleanup in CI workflows.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

📝 Walkthrough

Walkthrough

This pull request introduces infrastructure for optional bundling of V2D's robotic_grounding package into Teleop wheels. A new GitHub Actions composite action (setup-v2d-src) reads a pinned commit SHA from deps/v2d/version.txt, clones the V2D repository, and populates deps/v2d/src/robotic_grounding. The CMake build system gains a BUNDLE_ROBOTIC_GROUNDING flag; when enabled, the package is staged into wheels. A post-build script strips robotic_grounding from Release wheel artifacts for public CI. A new SharpaHandRetargeter implementation wraps V2D's hand kinematics to convert OpenXR hand tracking into Sharpa joint angles, supported by setup scripts, requirements files, tests, documentation, and a demo example.

Sequence Diagram

sequenceDiagram
    actor GHA as GitHub Actions<br/>(build-ubuntu.yml)
    participant ACT as setup-v2d-src<br/>Action
    participant REPO as jiwenc-nv/v2d<br/>Repository
    participant CACHE as GitHub<br/>Cache
    participant CMAKE as CMake<br/>Build System
    participant WHEEL as Wheel<br/>Builder
    participant STRIP as strip_robotic<br/>_grounding.py

    GHA->>ACT: Trigger (non-release branch)
    ACT->>ACT: Read deps/v2d/version.txt
    alt Cache Hit
        ACT->>CACHE: Restore by SHA key
        CACHE-->>ACT: robotic_grounding/src
    else Cache Miss
        ACT->>REPO: Clone retargeter branch
        REPO-->>ACT: V2D repository
        ACT->>ACT: Checkout pinned SHA
        ACT->>ACT: Copy robotic_grounding/source<br/>into deps/v2d/src/
        ACT->>CACHE: Save by SHA key
    end
    ACT->>GHA: Output bundled=true/false

    GHA->>CMAKE: Configure with<br/>BUNDLE_ROBOTIC_GROUNDING=TRUE

    CMAKE->>CMAKE: Verify __init__.py exists
    CMAKE->>WHEEL: Build wheel with<br/>robotic_grounding staged

    alt Release Build
        WHEEL-->>GHA: Release wheel artifact
        GHA->>STRIP: Post-build strip phase
        STRIP->>STRIP: Remove robotic_grounding/<br/>entries, rewrite RECORD
        STRIP-->>GHA: Cleaned wheel
    else Non-Release Build
        WHEEL-->>GHA: Wheel (with grounding)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes


Critical observations

Wheel integrity & RECORD handling — The strip_robotic_grounding_from_wheel.py script manipulates wheel ZIP archives and recalculates SHA-256 hashes for the RECORD file. Verify:

  • All non-deleted files are correctly re-hashed (no silent data corruption)
  • The RECORD file itself gets the canonical empty-hash row per PEP 427
  • Atomic file replacement (move after successful rebuild) prevents partial writes

CMake conditional logic — When BUNDLE_ROBOTIC_GROUNDING=TRUE, the build hard-fails if deps/v2d/src/robotic_grounding/__init__.py is missing. Confirm:

  • The __init__.py check fires early enough to prevent wheel staging on failure
  • CI workflow only enables this flag when setup-v2d-src confirms bundled=true

Version pinning & CI gating — The setup action only fetches when github-token is non-empty (prevents token leakage in forks). Ensure:

  • Workflows pass tokens only on trusted runners
  • Version.txt is always committed (no silent skips on missing file)

Warm-start state lifecycleSharpaHandRetargeter persists _qpos_prev across compute calls and resets on invalid input. Verify:

  • No stale IK state leaks across retargeter reuse or between left/right hands
  • Reset behavior matches the documented contract in tests
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: integrating the Sharpa retargeter from the V2D repository into IsaacTeleop.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Review rate limit: 9/10 reviews remaining, refill in 6 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

📝 Docs preview is not auto-deployed for fork PRs.

A maintainer with write access to NVIDIA/IsaacTeleop can deploy a preview by
commenting /preview-docs on this PR. Once deployed, the preview
will live at:

https://nvidia.github.io/IsaacTeleop/preview/pr-444/

@jiwenc-nv jiwenc-nv requested a review from rwiltz April 30, 2026 18:24
@jiwenc-nv
Copy link
Copy Markdown
Collaborator Author

/preview-docs

@github-actions
Copy link
Copy Markdown
Contributor

❌ No successful Build & deploy docs run found for commit 3817deae. Wait for the build to finish (or push a commit that touches docs/ to trigger one), then re-comment /preview-docs.

@jiwenc-nv
Copy link
Copy Markdown
Collaborator Author

/preview-docs

@github-actions
Copy link
Copy Markdown
Contributor

✅ Preview deployed: https://NVIDIA.github.io/IsaacTeleop/preview/pr-444/

@jiwenc-nv jiwenc-nv force-pushed the sharpa_with_v2d branch 2 times, most recently from eb8baa3 to a4d9272 Compare May 1, 2026 06:57
@jiwenc-nv jiwenc-nv changed the title WIP: Bring in Sharpa retargeter from V2D Bring in Sharpa retargeter from V2D May 1, 2026
jiwenc-nv and others added 3 commits May 1, 2026 07:17
Note: V2D is still pre-release and gated from public access. The fetch
is gated on V2D_RETARGETER_TOKEN and skipped on release branches.
Wraps robotic_grounding.retarget.SharpaHandKinematics with Teleop's
BaseRetargeter contract -- OpenXR -> MANO joint mapping, warm-started
qpos, output indexing. Lazy-imports cleanly: installs without the
[grounding] extra raise a directed ModuleNotFoundError instead of
breaking module load.

Co-Authored-By: rwiltz <165190220+rwiltz@users.noreply.github.com>
Promotes references/retargeting.rst into a directory and adds
sharpa.rst covering [grounding] build, Python usage, the demo, and
ctest validation.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/strip_robotic_grounding_from_wheel.py`:
- Around line 91-93: The script currently logs and skips when a provided wheel
path fails the path.is_file() check (the block using "if not path.is_file(): ...
continue") which allows the script to later return success; change this so
invalid input paths cause a non-zero exit instead of continuing: either call
sys.exit(1) (or raise SystemExit(1)) immediately in that if-block, or set an
error flag when encountering any invalid path and after the loop call
sys.exit(1) if the flag is set, ensuring the script does not return success at
the final return on successful completion.

In `@src/core/retargeting_engine_tests/python/pyproject.toml`:
- Around line 24-35: The grounding extra lists "loop-rate-limiters" and "daqp"
without version constraints which can pull incompatible older releases; update
the grounding array in pyproject.toml to add minimum version specifiers that
match src/core/python/requirements-grounding.txt (or the tested minimums), e.g.
add "loop-rate-limiters>=<min_version>" and "daqp>=<min_version>" so that the
grounding list (the grounding = [...] block) enforces the same minimum versions
as the requirements file.

In `@src/retargeters/sharpa_hand_retargeter.py`:
- Around line 284-290: Before mapping, detect any overlapping names between
left_joint_names and right_joint_names (e.g. overlap = set(left_joint_names) &
set(right_joint_names)); if overlap is non-empty, raise an error (ValueError or
custom) listing the offending joint names so the ambiguous names are rejected
instead of silently favoring left side. Place this check immediately before the
loop that populates _output_indices_left/_output_indices_right and
_left_indices/_right (i.e., prior to the for i, jname in
enumerate(target_joint_names) loop) so mapping only proceeds when left/right
sets are disjoint.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 8f79a831-aa0d-4fd5-a8e5-11722990e50f

📥 Commits

Reviewing files that changed from the base of the PR and between a939dd1 and ceb85ce.

📒 Files selected for processing (19)
  • .github/actions/setup-v2d-src/action.yml
  • .github/workflows/build-ubuntu.yml
  • .gitignore
  • deps/v2d/version.txt
  • docs/source/index.rst
  • docs/source/references/retargeting/index.rst
  • docs/source/references/retargeting/sharpa.rst
  • examples/retargeting/python/sharpa_hand_retargeter_demo.py
  • scripts/setup_v2d_src.sh
  • scripts/strip_robotic_grounding_from_wheel.py
  • src/core/python/CMakeLists.txt
  • src/core/python/pyproject.toml.in
  • src/core/python/requirements-grounding.txt
  • src/core/python/requirements-retargeters.txt
  • src/core/retargeting_engine_tests/python/CMakeLists.txt
  • src/core/retargeting_engine_tests/python/pyproject.toml
  • src/core/retargeting_engine_tests/python/test_sharpa_hand_retargeter.py
  • src/retargeters/__init__.py
  • src/retargeters/sharpa_hand_retargeter.py

Comment on lines +91 to +93
if not path.is_file():
print(f" {path}: not a file, skipping", file=sys.stderr)
continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed when an input wheel path is invalid.

Line 91-Line 93 currently logs and skips invalid paths, then Line 101 still returns success. For a release-safety stripping step, this can silently bypass stripping when path resolution is wrong.

Proposed fix
 def main(argv: list[str]) -> int:
@@
-    any_modified = False
+    any_modified = False
+    had_error = False
     for w in args.wheels:
         path = Path(w)
         if not path.is_file():
             print(f"  {path}: not a file, skipping", file=sys.stderr)
+            had_error = True
             continue
         if strip_wheel(path):
             any_modified = True
@@
-    return 0
+    return 1 if had_error else 0

Also applies to: 101-101

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/strip_robotic_grounding_from_wheel.py` around lines 91 - 93, The
script currently logs and skips when a provided wheel path fails the
path.is_file() check (the block using "if not path.is_file(): ... continue")
which allows the script to later return success; change this so invalid input
paths cause a non-zero exit instead of continuing: either call sys.exit(1) (or
raise SystemExit(1)) immediately in that if-block, or set an error flag when
encountering any invalid path and after the loop call sys.exit(1) if the flag is
set, ensuring the script does not return success at the final return on
successful completion.

Comment on lines +24 to +35
# Mirrors src/core/python/requirements-grounding.txt. The Sharpa unit test
# imports robotic_grounding (staged into PYTHONPATH from the wheel build);
# its runtime deps come from this extra.
grounding = [
"numpy",
"scipy",
"torch>=2.7.0",
"pin>=2.7",
"pin-pink>=4.0",
"loop-rate-limiters",
"daqp",
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Canonical grounding requirements =="
cat src/core/python/requirements-grounding.txt | sed -n '1,40p'

echo
echo "== Test pyproject grounding extra =="
cat src/core/retargeting_engine_tests/python/pyproject.toml | sed -n '20,45p'

echo
echo "== Quick diff on relevant packages =="
python - <<'PY'
from pathlib import Path
req = Path("src/core/python/requirements-grounding.txt").read_text()
py = Path("src/core/retargeting_engine_tests/python/pyproject.toml").read_text()

targets = ["pin", "pin-pink", "loop-rate-limiters", "daqp"]
print("requirements-grounding.txt:")
for t in targets:
    for line in req.splitlines():
        if line.strip().startswith(t):
            print("  ", line.strip())

print("\npyproject grounding extra:")
inside=False
for line in py.splitlines():
    s=line.strip()
    if s.startswith("grounding = ["):
        inside=True
        continue
    if inside and s=="]":
        break
    if inside:
        for t in targets:
            if t in s:
                print("  ", s.strip().strip('",'))
PY

Repository: NVIDIA/IsaacTeleop

Length of output: 1999


Missing version constraints for loop-rate-limiters and daqp in grounding extra

The grounding extra claims to mirror src/core/python/requirements-grounding.txt, but loop-rate-limiters and daqp are listed without minimum version constraints. This allows installation of older incompatible versions, risking test failures and nondeterministic environment setup.

Fix
 grounding = [
     "numpy",
     "scipy",
     "torch>=2.7.0",
     "pin>=2.7",
     "pin-pink>=4.0",
-    "loop-rate-limiters",
-    "daqp",
+    "loop-rate-limiters>=1.0.0",
+    "daqp>=0.5.0",
 ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/retargeting_engine_tests/python/pyproject.toml` around lines 24 -
35, The grounding extra lists "loop-rate-limiters" and "daqp" without version
constraints which can pull incompatible older releases; update the grounding
array in pyproject.toml to add minimum version specifiers that match
src/core/python/requirements-grounding.txt (or the tested minimums), e.g. add
"loop-rate-limiters>=<min_version>" and "daqp>=<min_version>" so that the
grounding list (the grounding = [...] block) enforces the same minimum versions
as the requirements file.

Comment on lines +284 to +290
for i, jname in enumerate(target_joint_names):
if jname in left_joint_names:
self._output_indices_left.append(i)
self._left_indices.append(left_joint_names.index(jname))
elif jname in right_joint_names:
self._output_indices_right.append(i)
self._right_indices.append(right_joint_names.index(jname))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject overlapping left/right joint names before mapping.

Line 285-Line 290 silently prioritizes left_joint_names when a name exists in both sides, so right-hand values can be dropped without error.

Proposed fix
         self._output_indices_left: list[int] = []
         self._output_indices_right: list[int] = []
 
+        overlap = set(left_joint_names) & set(right_joint_names)
+        if overlap:
+            raise ValueError(
+                f"left_joint_names and right_joint_names overlap: {sorted(overlap)}"
+            )
+
         for i, jname in enumerate(target_joint_names):
             if jname in left_joint_names:
                 self._output_indices_left.append(i)
                 self._left_indices.append(left_joint_names.index(jname))
             elif jname in right_joint_names:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/retargeters/sharpa_hand_retargeter.py` around lines 284 - 290, Before
mapping, detect any overlapping names between left_joint_names and
right_joint_names (e.g. overlap = set(left_joint_names) &
set(right_joint_names)); if overlap is non-empty, raise an error (ValueError or
custom) listing the offending joint names so the ambiguous names are rejected
instead of silently favoring left side. Place this check immediately before the
loop that populates _output_indices_left/_output_indices_right and
_left_indices/_right (i.e., prior to the for i, jname in
enumerate(target_joint_names) loop) so mapping only proceeds when left/right
sets are disjoint.

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.

1 participant