Skip to content

Add rigid object constraint support (attach/detach)#337

Open
yuecideng wants to merge 17 commits into
mainfrom
feat/rigid-constraint
Open

Add rigid object constraint support (attach/detach)#337
yuecideng wants to merge 17 commits into
mainfrom
feat/rigid-constraint

Conversation

@yuecideng

Copy link
Copy Markdown
Contributor

Description

This PR adds support for attaching two RigidObjects via a fixed physics constraint and removing it, exposed both as a standalone SimulationManager API (usable outside the gym) and as on-demand event functors triggered from a task environment. It also includes a two-cube tutorial.

What's added:

  • RigidConstraintCfg (embodichain/lab/sim/cfg.py) — names the constraint, points at two object UIDs, optional local joint frames, and a reserved constraint_type field (fixed-only for v1; prismatic/revolute/spherical/d6 can land later without API change).
  • RigidConstraint wrapper (embodichain/lab/sim/objects/constraint.py) — a batch wrapper mirroring RigidObject's per-arena pattern: one dexsim FixedConstraint handle per arena, with None where inactive so arena-index == list-index. Exposes get_relative_transform, get_local_pose, is_valid, destroy (all env_ids-aware).
  • SimulationManager API (sim_manager.py) — create_rigid_constraint / remove_rigid_constraint / get_rigid_constraint + a _constraints registry, wired into asset_uids and _deferred_destroy. env_ids-aware, so a vectorized task can attach/detach a subset of arenas.
  • Event functors (managers/events.py) — create_rigid_constraint / remove_rigid_constraint thin adapters (resolve SceneEntityCfgRigidObject → sim API), triggered via custom modes (event_manager.apply(mode="attach"/"detach", env_ids)).
  • Tutorialscripts/tutorials/sim/create_rigid_constraint.py + docs/source/tutorial/rigid_constraint.rst (two cubes via CubeCfg, no asset file needed; prints the bodies' relative z while attached vs after removal).

Design choice worth highlighting: with default (None) local frames, the constraint welds the two objects at their current relative poselocal_frame_a defaults to identity and local_frame_b is computed per env as inv(pose_B) @ pose_A. This matches the grasp/attach use case ("attach where the object is") rather than pulling the two origins together. (Confirmed by a real headless run: relative z held constant at −0.2 while attached, drifts to −0.16 after removal.)

Motivation: EmbodiChain had no physics constraint between rigid bodies. Constraints are needed for grasping (weld a held object to the gripper) and assembly (temporarily join two parts), and for reset-safe episode logic (detach before reset).

Dependencies: None new. Uses the existing dexsim create_fixed_constraint / remove_constraint API (bound on Arena).

Design spec: docs/superpowers/specs/2026-06-29-rigid-constraint-design.md; plan: docs/superpowers/plans/2026-06-29-rigid-constraint.md.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (non-breaking change which improves an existing functionality)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (existing functionality will not work without user modification)
  • Documentation update

Screenshots

Headless tutorial run (relative z = cube_b.z − cube_a.z, env 0):

ATTACHED:  step 0: -0.2000  step 40: -0.2000  step 120: -0.1996   (held constant)
DETACHED:  step 0: -0.1996  step 40: -0.1599  step 120: -0.1599   (drifts; gap closes)

Checklist

  • I have run the black . command to format the code base.
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • Dependencies have been updated, if applicable.

🤖 Generated with Claude Code

yuecideng and others added 13 commits June 28, 2026 16:01
Design for attaching two RigidObjects via a fixed physics constraint
and removing it, with a standalone sim-layer API (SimulationManager +
RigidConstraint) and an on-demand event functor in events.py.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Skips gracefully when the test asset is absent or CUDA is unavailable.
On a machine with the asset + GPU, asserts the fixed constraint holds
the relative transform under physics and that detaching lets objects
separate.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
With identity local frames the dexsim fixed constraint pulls the two body
origins together, which is wrong for the grasp/attach use case. When
local_frame_b is None, compute it per env as inv(pose_B) @ pose_A so the
constraint welds the objects at their current relative pose. local_frame_a
None still defaults to identity. Explicit local_frame_b is used verbatim.

Updates the spec, adds two unit tests, and makes the integration test's
detach assertion robust (relative-pose drift instead of absolute fall).

Co-Authored-By: Claude <noreply@anthropic.com>
Two-cube tutorial demonstrating create_rigid_constraint / remove via the
SimulationManager API, with a runnable script (CubeCfg, no asset file
needed) registered in the tutorial index. Prints the bodies' relative z
while attached (held constant) and after removal (free to drift).

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 29, 2026 03:52
@yuecideng yuecideng added physics Things related to physics object Simulation object assets dexsim Things related to dexsim gym robot learning env and its related features docs Improvements or additions to documentation labels Jun 29, 2026

Copilot AI left a comment

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.

Pull request overview

Adds first-class “attach/detach” support for RigidObjectRigidObject via fixed physics constraints, exposed both through the sim-layer SimulationManager API and through gym event functors, with tests and a tutorial to demonstrate usage.

Changes:

  • Introduces RigidConstraintCfg + RigidConstraint wrapper and wires a _constraints registry into SimulationManager with create/remove/get APIs.
  • Adds gym event functors (create_rigid_constraint / remove_rigid_constraint) to trigger attach/detach on demand from tasks.
  • Adds unit tests, an integration smoke test, and new tutorial docs + example script.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
embodichain/lab/sim/cfg.py Adds RigidConstraintCfg config object for constraints.
embodichain/lab/sim/objects/constraint.py Adds RigidConstraint batched wrapper over per-arena dexsim handles.
embodichain/lab/sim/objects/__init__.py Exports RigidConstraint from the sim objects package.
embodichain/lab/sim/sim_manager.py Adds constraint registry + create/remove/get_rigid_constraint APIs and cleanup wiring.
embodichain/lab/gym/envs/managers/events.py Adds attach/detach event functors that delegate into the sim API.
tests/sim/objects/test_rigid_constraint.py Adds mock-based unit tests for cfg/wrapper and sim-manager constraint plumbing.
tests/gym/envs/managers/test_event_rigid_constraint.py Adds unit tests for new rigid-constraint event functors and EventManager custom mode application.
tests/sim/test_rigid_constraint_integration.py Adds real-sim smoke test validating attach holds relative pose and detach allows separation.
scripts/tutorials/sim/create_rigid_constraint.py Adds runnable tutorial script demonstrating attach/detach with two cubes.
docs/source/tutorial/rigid_constraint.rst Adds tutorial page documenting the workflow and referencing the script.
docs/source/tutorial/index.rst Adds the new rigid-constraint tutorial to the tutorial index/toctree.
docs/superpowers/specs/2026-06-29-rigid-constraint-design.md Adds design spec for the feature.
docs/superpowers/plans/2026-06-29-rigid-constraint.md Adds implementation plan document.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +654 to +670
obj_a = env.sim.get_asset(obj_a_cfg.uid)
obj_b = env.sim.get_asset(obj_b_cfg.uid)
if not isinstance(obj_a, RigidObject) or not isinstance(obj_b, RigidObject):
logger.log_error(
f"Constraint '{name}' requires two RigidObjects, but got "
f"{type(obj_a).__name__} and {type(obj_b).__name__}."
)
env.sim.create_rigid_constraint(
cfg=RigidConstraintCfg(
name=name,
rigid_object_a_uid=obj_a_cfg.uid,
rigid_object_b_uid=obj_b_cfg.uid,
local_frame_a=local_frame_a,
local_frame_b=local_frame_b,
),
env_ids=env_ids,
)
env_ids: Target environment indices. None -> all envs.
name: Base constraint name to remove.
"""
env.sim.remove_rigid_constraint(name, env_ids=env_ids)
Comment on lines +1012 to +1017
local_frame_a: 4x4 joint frame in object A's local coordinates.
``None`` attaches at the objects' current relative pose (identity).
Accepts a single ``(4, 4)`` matrix (shared by all envs) or an
``(N, 4, 4)`` array (one frame per env). Defaults to None.
local_frame_b: As :attr:`local_frame_a`, for object B. Defaults to None.
constraint_type: Reserved for future typed constraints (prismatic,
Comment on lines +1034 to +1039
local_frame_a: np.ndarray | None = None
"""Local joint frame on object A. None -> identity (current relative pose)."""

local_frame_b: np.ndarray | None = None
"""Local joint frame on object B. None -> identity (current relative pose)."""

Comment on lines +647 to +650
local_frame_a: Local joint frame on object A. None attaches at the
objects' current relative pose. Accepts (4,4) or (N,4,4).
local_frame_b: Local joint frame on object B. None -> identity.

assert all(h is None for h in constraint.constraint_handles)


from embodichain.lab.sim.sim_manager import SimulationManager
Comment on lines +146 to +148
from embodichain.lab.gym.envs.managers.event_manager import EventManager
from embodichain.lab.gym.envs.managers.cfg import EventCfg
from embodichain.utils import configclass
Comment on lines +76 to +78
duck_path = get_data_path(DUCK_PATH)
# Two dynamic ducks at different heights, welded at identity frames.
attrs_a = RigidBodyAttributesCfg()
Comment on lines +101 to +105
if sim_device == "cuda" and getattr(self.sim, "is_use_gpu_physics", False):
self.sim.init_gpu_physics()
self.sim.enable_physics(True)

def test_fixed_constraint_holds_relative_pose(self):
Comment thread tests/gym/envs/managers/test_event_rigid_constraint.py Outdated
yuecideng and others added 3 commits June 29, 2026 15:44
- Normalize env_ids (None / tensor / sequence) to list[int] at the sim
  layer via _normalize_env_ids, used in create_rigid_constraint and
  remove_rigid_constraint. Widens the sim API to accept torch.Tensor
  (as passed by EventManager) so the per-arena dexsim names are clean
  ('weld_0' not a tensor stringification) and create/remove agree.
  Adds tensor-env_ids regression tests.
- Correct local_frame_b docstrings/comments across RigidConstraintCfg,
  the create functor, the tutorial script, and the integration test:
  None -> inv(pose_B) @ pose_A (weld at current relative pose), not
  identity. local_frame_a None stays identity.
- Make RigidConstraint.rigid_object_a/b optional in the type hint
  (RigidObject | None) to match their None defaults.
- Move mid-file test imports to module top (E402); drop an unused
  RigidObject import.
- Add teardown_method to the integration base test (destroy + flush) to
  avoid leaking dexsim scenes between tests.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 29, 2026 08:04

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Comment on lines 424 to 442
def asset_uids(self) -> List[str]:
"""Get all assets uid in the simulation.

The assets include lights, sensors, robots, rigid objects and articulations.

Returns:
List[str]: list of all assets uid.
"""
uid_list = ["default_plane"]
uid_list.extend(list(self._lights.keys()))
uid_list.extend(list(self._sensors.keys()))
uid_list.extend(list(self._robots.keys()))
uid_list.extend(list(self._rigid_objects.keys()))
uid_list.extend(list(self._rigid_object_groups.keys()))
uid_list.extend(list(self._soft_objects.keys()))
uid_list.extend(list(self._cloth_objects.keys()))
uid_list.extend(list(self._articulations.keys()))
uid_list.extend(list(self._constraints.keys()))
return uid_list
Comment on lines +1178 to +1182
if handle is None:
logger.log_error(
f"Failed to create constraint '{name_i}' in arena {env_id}."
)
handles[env_id] = handle
Comment on lines +63 to +68
sim_cfg = SimulationManagerCfg(
width=1920,
height=1080,
headless=True,
physics_dt=1.0 / 100.0, # Physics timestep (100 Hz)
sim_device=args.device,
import argparse
import sys

import numpy as np
Comment on lines +131 to +133
sim.remove_rigid_constraint("cube_weld")
assert sim.get_rigid_constraint("cube_weld") is None
print("\n[INFO]: Removed constraint 'cube_weld'. cube_a and cube_b are now free.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dexsim Things related to dexsim docs Improvements or additions to documentation gym robot learning env and its related features object Simulation object assets physics Things related to physics

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants