Skip to content

feat(manipulation): arm reachability capability map + viser viewer#2477

Open
Nabla7 wants to merge 3 commits into
mainfrom
pim/feat/g1-reachability
Open

feat(manipulation): arm reachability capability map + viser viewer#2477
Nabla7 wants to merge 3 commits into
mainfrom
pim/feat/g1-reachability

Conversation

@Nabla7

@Nabla7 Nabla7 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Problem

A robot arm needs a fast, offline answer to "can it reach this pose, and how dexterously?" — for grasp planning, base placement, and feasibility ghosts in the viser GUI — without paying an IK solve per query.

Solution

A reachability capability map. construct FK-samples an arm over millions of configurations and bins the end-effector poses into a voxel grid in the base frame, recording which orientations are reachable per cell as a dexterity score — a single .npz that answers reachability/dexterity in O(1). evaluate scores a built map against a mink solve-to-convergence IK oracle; viewer renders it in viser, colored by dexterity, with the robot posed at the map origin.

It's robot-agnostic: a small registry (reachability/robots.py) maps a --robot key to a MuJoCo-loadable model — MJCF or URDF (URDF loaded collision-only with package:// expanded) — with per-arm workspace auto-sizing and exclusions for constant structural mesh overlaps. Ships six arms as examples: g1-left/right, xarm7, piper, a750 (URDF), openarm (URDF). Self-contained — loads the model directly and runs its own FK/IK, with no dependency on the planning/world stack.

How to Test

# build + view a map for any registered arm (xarm7 shown)
uv run --extra ik python -m dimos.manipulation.reachability.construct --robot xarm7 --samples 200000
uv run --extra ik python -m dimos.manipulation.reachability.viewer --map data/reachability/xarm7_capability.npz

--robot accepts g1-left, g1-right, xarm7, piper, a750, openarm.

Contributor License Agreement

  • I have read and approved the CLA.

@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 47.50000% with 420 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
dimos/manipulation/reachability/viewer.py 11.26% 321 Missing and 2 partials ⚠️
...s/manipulation/reachability/test_capability_map.py 71.28% 56 Missing ⚠️
dimos/manipulation/reachability/capability_map.py 80.00% 36 Missing and 3 partials ⚠️
dimos/manipulation/reachability/robots.py 95.65% 1 Missing and 1 partial ⚠️
@@            Coverage Diff             @@
##             main    #2477      +/-   ##
==========================================
- Coverage   69.97%   69.72%   -0.25%     
==========================================
  Files         842      846       +4     
  Lines       74420    75370     +950     
  Branches     6668     6777     +109     
==========================================
+ Hits        52077    52554     +477     
- Misses      20636    21124     +488     
+ Partials     1707     1692      -15     
Flag Coverage Δ
OS-ubuntu-24.04-arm 63.69% <44.87%> (-0.22%) ⬇️
OS-ubuntu-latest 64.52% <44.87%> (-0.22%) ⬇️
Py-3.10 64.52% <44.87%> (-0.22%) ⬇️
Py-3.11 64.51% <44.87%> (-0.22%) ⬇️
Py-3.12 64.51% <44.87%> (-0.22%) ⬇️
Py-3.13 64.52% <44.87%> (-0.22%) ⬇️
Py-3.14 64.52% <44.87%> (-0.23%) ⬇️
Py-3.14t 64.52% <44.87%> (-0.22%) ⬇️
SelfHosted-Large 30.22% <18.71%> (-0.07%) ⬇️
SelfHosted-macOS 36.93% <28.66%> (-0.06%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
dimos/manipulation/reachability/robots.py 95.65% <95.65%> (ø)
dimos/manipulation/reachability/capability_map.py 80.00% <80.00%> (ø)
...s/manipulation/reachability/test_capability_map.py 71.28% <71.28%> (ø)
dimos/manipulation/reachability/viewer.py 11.26% <11.26%> (ø)

... and 13 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Nabla7 Nabla7 force-pushed the pim/feat/g1-reachability branch 3 times, most recently from a37d624 to 81d32be Compare June 16, 2026 21:45
@Nabla7 Nabla7 changed the title Pim/feat/g1 reachability feat(manipulation): G1 arm reachability capability map + viser viewer Jun 16, 2026
@Nabla7 Nabla7 force-pushed the pim/feat/g1-reachability branch 2 times, most recently from 9763050 to b22fe65 Compare June 16, 2026 22:59
@Nabla7 Nabla7 changed the title feat(manipulation): G1 arm reachability capability map + viser viewer feat(manipulation): arm reachability capability map + viser viewer Jun 16, 2026
@Nabla7 Nabla7 force-pushed the pim/feat/g1-reachability branch 6 times, most recently from cbd95b3 to ce11dcc Compare June 17, 2026 06:28
A reachability capability map for robot arms: FK-sample an arm to build
a dexterity grid in the base frame, mink-IK-verify it, and view it in
viser. Self-contained — loads the arm model directly and does its own
FK/IK, with no dependency on the sim/world planning stack or the
entity-scene contract.

Robot-agnostic via a small registry (reachability/robots.py): MJCF or
URDF models (URDF loaded collision-only with package:// expanded),
workspace grids auto-sized per arm, and per-arm exclusions for constant
structural mesh overlaps. Ships six arms as examples — g1-left/right,
xarm7, piper, a750 (URDF), openarm (URDF) — selected by
construct/evaluate/viewer --robot <key>.

Adds the 'ik' extra (daqp, mink==1.1.0, viser[urdf]) and mujoco MjSpec
type stubs.
@Nabla7 Nabla7 force-pushed the pim/feat/g1-reachability branch from ce11dcc to 829789c Compare June 17, 2026 06:46
`_REGISTRY` built its `package_roots` with `str(LfsPath(...))`, and `str()`
forces LfsPath's lazy download eagerly — so merely importing `robots` pulled
`g1_urdf.tar.gz` (the first entry). That fails on the LFS-guarded main CI
runner (bin/git-lfs-guard), taking down every test that imports the module,
including `test_registry_is_consistent`, a pure registry check that needs no
assets.

- Store `LfsPath` objects in `package_roots` (compile_model stringifies them
  at compile time, so the pull stays deferred); widen to `dict[str, str | Path]`.
- Mark `test_registered_model_compiles` `self_hosted`: it compiles real models
  pulled from the self-hosted LFS, so it belongs on the self-hosted CI job, not
  the main runner where git-lfs is guarded.

Main job: registry + pure-logic tests pass, the 6 compiles deselected.
Self-hosted job: the 6 compiles pull + compile + pass (verified locally).
@Nabla7 Nabla7 marked this pull request as ready for review June 18, 2026 16:55
@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a robot-agnostic reachability capability map for arm manipulation: FK-sampling millions of random configurations builds a 5D voxel grid (CapabilityMap) that answers reachability and dexterity queries in O(1), serialized to .npz. A viser-based interactive viewer renders the workspace colored by dexterity and drives live mink IK on a URDF ghost.

  • capability_map.py defines the 5D grid, the heading-quotiented canonical-value transform, saturating uint8 counts, body-frame companions, and .npz save/load.
  • construct.py FK-samples arms in parallel (multiprocessing spawn), merges per-worker maps, and auto-sizes the grid from the collision-free workspace bounding box.
  • robots.py is a registry of six arms (G1 L/R, xarm7, piper, a750, openarm) mapping robot keys to MJCF/URDF models.
  • evaluate.py validates the map against a mink IK oracle with random restarts.
  • viewer.py provides an interactive viser viewer with point-cloud/voxel display, vertical/horizontal slices, and a live IK gizmo.

Confidence Score: 4/5

Safe to merge. The capability-map math, canonical-value transform, and saturating merge are well-tested; the new issues are limited to a robustness gap in URDF injection and two low-probability viewer threading edge cases.

The core map construction, canonical-value transform, body-frame dexterity, mirror symmetry, and save/load are all thoroughly tested and the math checks out. The three findings are in the URDF compile helper and the interactive viewer: duplicate <mujoco> injection could cause a cryptic compile error for any future URDF that already embeds MuJoCo hints; the viewer swallows all scene-removal exceptions silently; and there is a narrow TOCTOU window on gizmo between the IK worker thread and the viser callback thread (caught by the outer handler, so the viewer keeps running). None of these affect map construction or query correctness.

dimos/manipulation/reachability/robots.py (URDF injection) and dimos/manipulation/reachability/viewer.py (exception handling and IK thread safety) deserve a second look before this ships to production robot deployments.

Important Files Changed

Filename Overview
dimos/manipulation/reachability/capability_map.py Core 5D capability map: canonical-value transform, saturating uint8 counts, body-frame companions, save/load. Math and index bounds handling look correct; np.bitwise_count requires NumPy ≥ 1.25, which the project's floor (1.26.4) satisfies.
dimos/manipulation/reachability/construct.py FK-sampling construction with spawn-based multiprocessing and grid auto-sizing. The Python for-loop in sample_chunk over 50K configs is the expected bottleneck. _autosize runs a 30K-sample probe on first arm_spec() call in CLI then discards the result — logically harmless but wasteful.
dimos/manipulation/reachability/robots.py compile_model injects <mujoco><compiler …/></mujoco> into URDF without first checking for an existing <mujoco> element — a duplicate would cause a MuJoCo compile error for any URDF that already carries such a section.
dimos/manipulation/reachability/viewer.py Interactive viser viewer. Two issues: broad except Exception: pass silences all scene-node removal errors in refresh_volume/refresh_slices; TOCTOU race on gizmo between the IK worker thread and viser's callback thread (low-probability crash, caught by outer handler).
dimos/manipulation/reachability/evaluate.py IK-oracle evaluation harness with mink QP, random restarts, and per-theta-bin FPR breakdown. Logically sound; heading canonicalization aligns evaluation semantics with map construction.
dimos/manipulation/reachability/test_capability_map.py Good unit-test coverage: yaw gauge invariance, pole finiteness, record/query roundtrip, saturation, mirror identity, save/load, viewer geometry, and per-robot model compilation smoke tests.
pyproject.toml Adds the new ik optional extra (daqp, mink==1.1.0, viser[urdf]) and updates mypy ignore list. Pinning mink==1.1.0 to hold MuJoCo floor is documented in a comment.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CLI as construct CLI
    participant AS as arm_spec / _autosize
    participant W as _worker (spawn)
    participant S as _ArmSampler (FK+collision)
    participant CM as CapabilityMap
    participant NPZ as .npz file

    CLI->>AS: arm_spec(robot) — probe 30K samples if no fixed params
    AS-->>CLI: ConstructionSpec (with sized MapParams)
    CLI->>W: pool.map(_worker, jobs) × n_workers
    W->>S: _ArmSampler(spec)
    loop per chunk (50K configs)
        W->>S: sample_chunk(n, rng)
        S->>S: mj_kinematics + mj_collision
        S-->>W: (positions, rotations, rejected)
        W->>CM: record_batch → saturating uint8 counts + heading_hint + body_counts + body_theta_mask
    end
    W-->>CLI: (CapabilityMap, rejected)
    CLI->>CM: merge workers (OR hints, clip counts to 255)
    CM->>NPZ: save(.npz)

    Note over CLI,NPZ: O(1) query path
    CM->>CM: scores(positions, rotations) — canonical_values → 5D index lookup
    CM->>CM: body_dexterity() — bitcount(body_theta_mask) / n_theta
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant CLI as construct CLI
    participant AS as arm_spec / _autosize
    participant W as _worker (spawn)
    participant S as _ArmSampler (FK+collision)
    participant CM as CapabilityMap
    participant NPZ as .npz file

    CLI->>AS: arm_spec(robot) — probe 30K samples if no fixed params
    AS-->>CLI: ConstructionSpec (with sized MapParams)
    CLI->>W: pool.map(_worker, jobs) × n_workers
    W->>S: _ArmSampler(spec)
    loop per chunk (50K configs)
        W->>S: sample_chunk(n, rng)
        S->>S: mj_kinematics + mj_collision
        S-->>W: (positions, rotations, rejected)
        W->>CM: record_batch → saturating uint8 counts + heading_hint + body_counts + body_theta_mask
    end
    W-->>CLI: (CapabilityMap, rejected)
    CLI->>CM: merge workers (OR hints, clip counts to 255)
    CM->>NPZ: save(.npz)

    Note over CLI,NPZ: O(1) query path
    CM->>CM: scores(positions, rotations) — canonical_values → 5D index lookup
    CM->>CM: body_dexterity() — bitcount(body_theta_mask) / n_theta
Loading

Reviews (1): Last reviewed commit: "Merge branch 'main' into pim/feat/g1-rea..." | Re-trigger Greptile

Comment on lines +197 to +202
text = re.sub(
r"(<robot\b[^>]*>)",
r'\1<mujoco><compiler strippath="false" discardvisual="true"/></mujoco>',
text,
count=1,
)

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.

P2 The URDF injection appends <mujoco>…</mujoco> after the <robot> opening tag without first checking whether the file already contains a <mujoco> element. Any URDF that embeds its own MuJoCo compiler hints (e.g., a community model that ships with strippath or mesh-directory overrides) will end up with two <mujoco> blocks, and MuJoCo's XML parser raises a compile error that is hard to diagnose. A pre-check or conditional replacement prevents the conflict.

Suggested change
text = re.sub(
r"(<robot\b[^>]*>)",
r'\1<mujoco><compiler strippath="false" discardvisual="true"/></mujoco>',
text,
count=1,
)
if "<mujoco>" not in text:
text = re.sub(
r"(<robot\b[^>]*>)",
r'\1<mujoco><compiler strippath="false" discardvisual="true"/></mujoco>',
text,
count=1,
)

Comment on lines +480 to +484
for name in ("/reachability/core", "/reachability/points"):
try:
server.scene.remove_by_name(name)
except Exception:
pass

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.

P2 Overly broad exception suppression on scene removal

except Exception: pass silences every possible failure, not just the "node does not exist" case. Viser can raise other exceptions (e.g., serialization errors, transport failures) that would be silently swallowed here and in refresh_slices, leaving the scene in a stale state with no indication of what went wrong. Narrowing the guard to the expected no-such-node error (or at minimum logging the unexpected ones) makes failures observable.

Comment on lines +571 to +588
def solve_current_pose() -> bool:
"""One solve at the gizmo's current pose; False if it failed."""
if gizmo is None:
return True
cap = current_map()
cap_robot = cap.robot or f"g1-{cap.side}"
if cap_robot not in solvers:
try:
solvers[cap_robot] = ArmIK(cap_robot)
except Exception as e:
solvers[cap_robot] = None
logger.warning(f"IK unavailable: {e}")
solver = solvers[cap_robot]
if solver is None:
ik_status.value = "IK unavailable (pip install 'dimos[ik]')"
return True
position = np.asarray(gizmo.position, dtype=np.float64)
wxyz = np.asarray(gizmo.wxyz, dtype=np.float64)

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.

P2 TOCTOU race on gizmo between IK worker and viser callback

solve_current_pose checks if gizmo is None: return True on line 573, then reads gizmo.position and gizmo.wxyz on lines 587–588. The viser callback thread can call refresh_ik between those two points and set gizmo = None (line 549), causing an AttributeError. In practice the outer except Exception in ik_worker catches it, so the viewer survives, but the IK thread silently stops until the next wakeup event. A second if gizmo is None guard immediately before the attribute accesses closes the window.

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