Skip to content

Python: per-step MJCF actuator control + multibody state readback#12

Open
haixuanTao wants to merge 1 commit into
dimforge:mainfrom
haixuanTao:py-mjcf-actuation
Open

Python: per-step MJCF actuator control + multibody state readback#12
haixuanTao wants to merge 1 commit into
dimforge:mainfrom
haixuanTao:py-mjcf-actuation

Conversation

@haixuanTao

Copy link
Copy Markdown
Contributor

Makes MJCF robots drivable per control step from Python, with full state observation — the two pieces an RL / sim-to-sim evaluation loop needs. Follow-up to #7/#8, standalone on main.

What

ControlNexusState.apply_actuator_controls(viewer, ctrl, env=0) applies one MJCF control vector (one entry per <actuator>, in actuator order — NexusState.actuator_names() reports the order) with full MJCF actuator semantics via rapier3d-mjcf's apply_controls_multibody (<position> servos with kp/kv, <motor> force/gear, force limits), then pushes the refreshed joint-motor state to the GPU in one buffer write. The PD servo itself runs inside the GPU solver, so there is no per-step solver round-trip beyond that single upload.

ObservationNexusViewer.read_multibody_links(state, env=0) reads every link's state back in one readback: (coords, positions, quats, linvels, angvels) numpy arrays (one row per link, GPU traversal order). coords are the generalized joint coordinates (a revolute joint's angle is coords[3], the first angular slot); poses/velocities are world-space.

How

  • GpuMultibodySet::sync_joint_data_from_rapier refreshes each link's GenericJoint data (motors, limits) from the rapier multibody set, mirroring from_rapier's traversal order, then uploads links_static once. Only joint data is touched — coordinates/velocities/mass properties are untouched, so this can't teleport links.
  • NexusState::control_multibody_motors wraps "mutate rapier joints, then sync" without marking the world dirty (motor updates are control, not topology — no GPU rebuild).
  • links_workspace gains COPY_SRC + public accessors for the readback.
  • The Python NexusState keeps the MjcfRobotHandles from insert_mjcf.

Validation

On the LeRobot bipedal platform MJCF (12 position actuators, 13 links): a passive run collapses to torso z ≈ −0.34 within 1 s; driving the servos each step holds it at z ≈ −0.02, and read_multibody_links tracks joint angles/limits throughout. cargo check clean on nexus3d, nexus_viewer3d, nexus_python3d.

🤖 Generated with Claude Code

- GpuMultibodySet::sync_joint_data_from_rapier: refresh every link's joint
  data (motor targets/gains, limits) from the rapier multibody set in one
  buffer write, mirroring from_rapier's traversal order.
- NexusState::control_multibody_motors: runtime actuation entry point that
  mutates the rapier joints (e.g. rapier3d-mjcf's apply_controls_multibody)
  and pushes the refreshed joint data to the GPU, without marking the world
  dirty (no rebuild).
- Viewer::read_multibody_links + links_workspace COPY_SRC: one-readback
  joint coordinates, link world poses and world-space velocities per env.
- Python: NexusState keeps the MjcfRobotHandles from insert_mjcf and exposes
  actuator_names() / apply_actuator_controls(viewer, ctrl, env) with full
  MJCF actuator semantics; NexusViewer.read_multibody_links(state, env)
  returns (coords, positions, quats, linvels, angvels) numpy arrays.

Together these make MJCF robots drivable per control step from Python
(position-servo PD runs inside the solver) with full state observation --
the two pieces sim-to-sim eval loops need.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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