Skip to content

Pim/feat/g1 reachability#2477

Draft
Nabla7 wants to merge 4 commits into
mainfrom
pim/feat/g1-reachability
Draft

Pim/feat/g1 reachability#2477
Nabla7 wants to merge 4 commits into
mainfrom
pim/feat/g1-reachability

Conversation

@Nabla7

@Nabla7 Nabla7 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Problem

Manipulation planning is currently Drake-only and cannot serve the G1 properly:

  • No floating base: the pelvis gets welded to the world.
  • No grasp-center offsets: IK aims at the wrist link, about 13 cm behind the palm.
  • Planning context creation takes ~100 ms, which is too slow for probing thousands of states.
  • The collision world is a cook-time snapshot, so a moved chair is invisible to the planner.
  • There is no direct way to ask “is this pose reachable for the arm?” short of running IK and hoping.

Closes DIM-XXX

Solution

This PR adds four layers, split as one commit each.

1. Scene-entity contract

Adds EntityDescriptor for what an entity is, and EntityStateBatch for where everything is now.

This gives us a versioned wire format between whatever owns physics truth — sim today, perception later — and whatever mirrors the scene. It uses JSON-over-LCM, following the EntityMarkers pattern, so Rust/browser consumers can hand-decode it. Wire tests pin the schema.

entity_scene converts descriptors into MjSpec collision bodies, using cooked hulls when available and AABB boxes otherwise.

2. MujocoWorld

Adds a second WorldSpec backend using MuJoCo purely as a kinematics/collision library. It is never stepped.

Key points:

  • Scratch contexts are created in ~62 µs, compared to Drake’s ~100 ms.
  • Collision checks are scoped to the moving subtree of the planned joints, so feet-on-floor contacts do not veto arm plans.
  • Existing RRT-Connect and JacobianIK run unchanged.
  • A parity suite pins FK, Jacobian, collision, and IK agreement against DrakeWorld.

3. Wiring

ManipulationModule now has:

  • an entity_states port, so live poses flow into the planning world;
  • a world_backend config option.

This also adds a new G1 catalog entry:

  • dual arms are represented as views onto one shared MJCF;
  • G1 manipulation is MuJoCo-only for now.

DrakeWorld does not support the required grasp offsets or floating base behavior, so it raises instead of silently aiming IK at the wrist.

4. Reachability

Adds a G1 arm capability map in a gravity-aligned, heading-quotiented pelvis frame.

This is valid because the WBC gives us a true SE(2) base. The map is 5D rather than RM4D’s 4D because the G1’s ±92.5° wrist yaw breaks the 4D collapse.

Measured results:

  • 5D map: 4.9% false-positive rate
  • 4D marginal: 13.9% false-positive rate

Both are compared at equal recall and IK-verified.

Also included:

  • construction CLI;
  • evaluation CLI;
  • viser viewer with dexterity-colored workspace, slice planes, and live drag-to-reach IK ghost.

New Extras

Adds a new [ik] extra:

  • daqp
  • mink
  • viser

mink is pinned because 1.1.1+ forces MuJoCo 3.5 -> 3.9.

Out of Scope

Staged next:

  • the mink arm-control task for Cartesian reach;
  • scene-package cooking, the scene_package hook here is its first consumer;
  • an upstream entity-batch publisher.

Today’s producer is the pimsim branch. The new port is exercised by tests.

How to Test

Install dependencies:

uv sync --extra ik --extra sim --group tests

Run the test suite:

uv run pytest \
  dimos/manipulation/planning \
  dimos/manipulation/reachability \
  dimos/experimental/pimsim \
  dimos/simulation/mujoco/test_entity_scene.py \
  -m "mujoco or not mujoco"

Expected: 48 tests.

Note: the parity suite needs pydrake on Linux.

Build a reachability map and explore it:

uv run python -m dimos.manipulation.reachability.construct \
  --side left \
  --samples 2000000 \
  --workers 8 \
  --out /tmp/g1_left.npz

uv run python -m dimos.manipulation.reachability.viewer \
  --map /tmp/g1_left.npz

Reproduce the accuracy numbers:

uv run python -m dimos.manipulation.reachability.evaluate \
  --map /tmp/g1_left.npz \
  --poses 500

Sim regression check:

Run the G1 GR00T WBC MuJoCo sim as before. The world should be identical, minus the floating manipulation table/cube.

Contributor License Agreement

@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
2023 2 2021 38
View the top 2 failed test(s) by shortest run time
dimos.project.test_no_sections::test_no_section_markers
Stack Traces | 0.308s run time
def test_no_section_markers():
        """
        Fail if any file contains section-style comment markers.
    
        If a file is too complicated to be understood without sections, then the
        sections should be files. We don't need "subfiles".
        """
        violations = find_section_markers()
        if violations:
            report_lines = [
                f"Found {len(violations)} section marker(s). "
                "If a file is too complicated to be understood without sections, "
                'then the sections should be files. We don\'t need "subfiles".',
                "",
            ]
            for path, lineno, text in violations:
                report_lines.append(f"  {path}:{lineno}: {text.strip()}")
>           raise AssertionError("\n".join(report_lines))
E           AssertionError: Found 16 section marker(s). If a file is too complicated to be understood without sections, then the sections should be files. We don't need "subfiles".
E           
E             .../manipulation/reachability/viewer.py:65: # ----------------------------------------------------------------------
E             .../manipulation/reachability/viewer.py:191: # ----------------------------------------------------------------------
E             .../manipulation/reachability/viewer.py:364: # ----------------------------------------------------------------------
E             .../manipulation/reachability/capability_map.py:207: # ------------------------------------------------------------------
E             .../manipulation/reachability/capability_map.py:252: # ------------------------------------------------------------------
E             .../manipulation/reachability/capability_map.py:304: # ------------------------------------------------------------------
E             .../manipulation/reachability/capability_map.py:376: # ------------------------------------------------------------------
E             .../manipulation/reachability/capability_map.py:402: # ------------------------------------------------------------------
E             .../planning/world/mujoco_world.py:195: # ------------------------------------------------------------------
E             .../planning/world/mujoco_world.py:289: # ------------------------------------------------------------------
E             .../planning/world/mujoco_world.py:482: # ------------------------------------------------------------------
E             .../planning/world/mujoco_world.py:516: # ------------------------------------------------------------------
E             .../planning/world/mujoco_world.py:590: # ------------------------------------------------------------------
E             .../planning/world/mujoco_world.py:684: # ------------------------------------------------------------------
E             .../planning/world/mujoco_world.py:733: # ------------------------------------------------------------------
E             .../planning/world/mujoco_world.py:916: # ------------------------------------------------------------------

lineno     = 916
path       = '.../planning/world/mujoco_world.py'
report_lines = ['Found 16 section marker(s). If a file is too complicated to be understood without sections, then the sections should...ulation/reachability/capability_map.py:207: # ------------------------------------------------------------------', ...]
text       = '    # ------------------------------------------------------------------'
violations = [('.../manipulation/reachability/viewer.py', 65, '# ----------------------------------------------------------------...reachability/capability_map.py', 304, '    # ------------------------------------------------------------------'), ...]

dimos/project/test_no_sections.py:145: AssertionError
dimos.manipulation.planning.monitor.test_world_monitor_mujoco::test_g1_catalog_targets_grasp_center_not_wrist
Stack Traces | 3.14s run time
tmp_path = PosixPath('.../pytest-0/popen-gw0/test_g1_catalog_targets_grasp_0')

    def test_g1_catalog_targets_grasp_center_not_wrist(tmp_path: Path) -> None:
        """Sanity: the G1 catalog config carries a non-zero grasp offset — the
        EE pose must not silently regress to the wrist link origin. The drake
        backend is rejected until DrakeWorld grows grasp-offset support."""
        from dimos.robot.catalog.g1 import g1_left_arm, g1_right_arm
    
        mjc_cfg = g1_left_arm(backend="mujoco").robot_model_config
        assert np.linalg.norm(mjc_cfg.grasp_offset_xyz) > 0.05
        assert mjc_cfg.end_effector_link == "left_wrist_yaw_link"
>       assert str(mjc_cfg.model_path).endswith(".xml")

g1_left_arm = <function g1_left_arm at 0x7fd7faedb920>
g1_right_arm = <function g1_right_arm at 0x7fd6cf7a2480>
mjc_cfg    = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/mujoco_sim.tar.gz after 3 attempts: Co...fs/mujoco_sim.tar.gz']' returned non-zero exit status 1.") raised in repr()] RobotModelConfig object at 0x7fd5cdf2ab20>
tmp_path   = PosixPath('.../pytest-0/popen-gw0/test_g1_catalog_targets_grasp_0')

.../planning/monitor/test_world_monitor_mujoco.py:134: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/utils/data.py:369: in __str__
    return str(self._ensure_downloaded())
        self       = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/mujoco_sim.tar.gz after 3 attempts: Co... 'data/.lfs/mujoco_sim.tar.gz']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0x7fd5cd01cfd0>
dimos/utils/data.py:347: in _ensure_downloaded
    cache = get_data(filename)
        cache      = None
        filename   = 'mujoco_sim/g1_gear_wbc.xml'
        self       = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/mujoco_sim.tar.gz after 3 attempts: Co... 'data/.lfs/mujoco_sim.tar.gz']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0x7fd5cd01cfd0>
dimos/utils/data.py:304: in get_data
    archive_path = _decompress_archive(_pull_lfs_archive(archive_name))
        archive_name = 'mujoco_sim'
        data_dir   = PosixPath('.../dimos/dimos/data')
        file_path  = PosixPath('.../dimos/dimos/data/mujoco_sim/g1_gear_wbc.xml')
        name       = 'mujoco_sim/g1_gear_wbc.xml'
        nested_path = PosixPath('g1_gear_wbc.xml')
        path_parts = ('mujoco_sim', 'g1_gear_wbc.xml')
dimos/utils/data.py:248: in _pull_lfs_archive
    _lfs_pull(file_path, repo_root)
        file_path  = PosixPath('.../dimos/data/.lfs/mujoco_sim.tar.gz')
        filename   = 'mujoco_sim'
        repo_root  = PosixPath('.../work/dimos/dimos')
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

file_path = PosixPath('.../dimos/data/.lfs/mujoco_sim.tar.gz')
repo_root = PosixPath('.../work/dimos/dimos')

    def _lfs_pull(file_path: Path, repo_root: Path, *, retries: int = 2) -> None:
        relative_path = file_path.relative_to(repo_root)
    
        env = os.environ.copy()
        env["GIT_LFS_FORCE_PROGRESS"] = "1"
    
        last_err: subprocess.CalledProcessError | None = None
        for attempt in range(1, retries + 2):  # retries + 1 total attempts
            try:
                subprocess.run(
                    ["git", "lfs", "pull", "--include", str(relative_path)],
                    cwd=repo_root,
                    check=True,
                    env=env,
                )
                return
            except subprocess.CalledProcessError as e:
                last_err = e
                if attempt <= retries:
                    time.sleep(attempt)  # 1s, 2s backoff
    
>       raise RuntimeError(
            f"Failed to pull LFS file {file_path} after {retries + 1} attempts: {last_err}"
        )
E       RuntimeError: Failed to pull LFS file .../dimos/data/.lfs/mujoco_sim.tar.gz after 3 attempts: Command '['git', 'lfs', 'pull', '--include', 'data/.lfs/mujoco_sim.tar.gz']' returned non-zero exit status 1.

attempt    = 3
env        = {'ACCEPT_EULA': 'Y', 'ACTIONS_ID_TOKEN_REQUEST_TOKEN': 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjM4ODI2YjE3LTZhMzAtNWY5Yi1iMTY5LT...-version=2.0', 'ACTIONS_ORCHESTRATION_ID': 'eff6e9ef-7bca-4b0d-becf-d52374de0d0c.tests._3_12_ubuntu-latest_false', ...}
file_path  = PosixPath('.../dimos/data/.lfs/mujoco_sim.tar.gz')
last_err   = CalledProcessError(1, ['git', 'lfs', 'pull', '--include', 'data/.lfs/mujoco_sim.tar.gz'])
relative_path = PosixPath('data/.lfs/mujoco_sim.tar.gz')
repo_root  = PosixPath('.../work/dimos/dimos')
retries    = 2

dimos/utils/data.py:216: RuntimeError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@Nabla7 Nabla7 force-pushed the pim/feat/g1-reachability branch from 059b829 to c6dbdc8 Compare June 12, 2026 17:17
Nabla7 added 4 commits June 13, 2026 01:53
EntityDescriptor (what an entity is) + EntityStateBatch (where every
entity is): an authority-agnostic wire contract between whatever owns
physics truth (sim today, perception later) and whatever mirrors the
scene (planning worlds, visualizers, recorders). Versioned,
length-prefixed JSON over LCM — the EntityMarkers pattern — so
Rust/browser consumers hand-decode it without generated lcm_msgs types;
wire tests pin the schema. msg_name is namespaced simulation_msgs.*
(LCM channel keys derive from it) ahead of the type's eventual move out
of experimental.

entity_scene composes scene entities into an MjSpec — cooked collision
hulls when the package provides them, AABB boxes otherwise. The mujoco
lint stub gains the spec/kinematics surface this and downstream
consumers use.
A second WorldSpec implementation: MuJoCo as a kinematics/collision
library (never stepped). MjData scratch contexts are ~62 us to create vs
Drake's ~100 ms plant clones; collision checks scope to the planned
joints' moving subtree so a humanoid's feet-on-floor contacts don't veto
arm plans. RRT-Connect and JacobianIK run on it unchanged; the parity
suite pins FK/Jacobian/collision/IK agreement against DrakeWorld.

g1_gear_wbc.xml loses its embedded scene (floor, manip table/cube): a
planning world attaching the robot file would inherit that geometry at
the robot's base pose. The sim now loads g1_gear_wbc_scene.xml — ground
and office via include, no manipulables; props come from scene packages.
g1_urdf ships the meshes both consumers reference.
ManipulationModule grows world_backend/scene_package config and an
entity_states port: scene-entity poses stream from the physics authority
into the planning world, so plans route around obstacles where they are
now, not where the scene said they started. The G1 catalog (mujoco
backend only — DrakeWorld lacks grasp-offset/floating-base support)
registers dual arms as views onto one shared MJCF; WorldSpec.add_robot
gains the optional share_model_with capability (DrakeWorld raises).
RM4D-style capability map in a gravity-aligned, heading-quotiented
pelvis frame, extended with an explicit in-plane wrist-roll dimension
(the G1's +/-92.5 deg wrist_yaw breaks RM4D's 4D collapse: measured
false-positive rate 4.9% for the 5D map vs 13.9% for the 4D marginal at
equal recall). Construction FK-samples the same MJCF the sim uses and
rejects self-colliding configurations; an IK-verified evaluation
harness reports map accuracy. The viser viewer renders the body-frame
workspace red-to-green by dexterity with slice planes and a live
drag-to-reach IK ghost (collision-aware, search streamed to the ghost).

New [ik] extra: daqp + mink (pinned: 1.1.1+ raises the mujoco floor)
+ viser. mypy overrides for the untyped ik/viz deps.
@Nabla7 Nabla7 force-pushed the pim/feat/g1-reachability branch from c6dbdc8 to a37d624 Compare June 12, 2026 17:55
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